Motivation and Overview

This is a first pass exploration of different aspects of beer. The data was collected via the BreweryDB API. Special thanks to Kris Kroski for data ideation and co-membership in the honourable workplace beer consortium.

The main question this analysis is meant to tackle is: Are beer styles actually indicative of shared attributes of the beers within that style? Or are style boundaries more or less arbitrary? I took two approaches to this: unsupervised clustering and supervised prediction.

Clusters defined by the algorithm were compared to the style “centers” as defined by the mean ABV, IBU, and SRM. On the prediction side, predictor variables for include ABV (alcohol by volume), IBU (international bitterness units), SRM (a measure of color) as well as ingredients like hops and malts. The outcome variable is the style that beer was assigned.

This document starts off with an explanation of how I sourced beer data from BreweryDB, cleaned that data, and stuck the parts of it I wanted in a database. (These are just the highlights; the code actually executed in this document queries that database, specifically by sourcing the file read_from_db.R, also in this repo, rather than hitting the BreweryDB API. This is done for expediency’s sake as the code below detailing how to actually get the beer data, run in full in run_it.R, takes some time to execute.)

It then moves into clustering (k-means) and prediction (neural net, random forest). Below is a more detailed overview of the general workflow.

Workflow

1. Get and Prepare

When we first hit the BreweryDB API to iteratively pull in all beers and their ingredients along with other things we might want like breweries and glassware. Then we unnest the JSON responses, including all the ingredients columns, and dump this all into a MySQL database.

Next, we create a style_collapsed column to reduce the number of levels of our outcome variable. We do this by greping through each beer’s style to determine if that style contains a keyword that qualifies it to be rolled into a collapsed style; if it does, it gets that keyword in a style_collapsed column.

Finally we unnest the ingredients hops and malts into a wide, sparse dataframe. Individual ingredients are now columns, with each beer still in its own rows; a cell gets a 1 if ingredient is present and 0 otherwise. This allows more granual inference into ingredients’ effects on both style and bitterness (occasioning a short foray into hops).

2. Short foray into hops

A quick look at the most popular hops and an exploration of the relationship between hops and bitterness.

3. Infer

Cluster: unsupervised k-means clustering partitioning the entire dataset into ten clusters. Next, we cluster on a dataset composed of just five selected styles into five clusters.

We then attempt to predict predict either style or style_collapsed using a neural net and a random forest. The main predictors are ABV, IBU, SRM, total number of hops, and total number of malts. The glass a beer is served in is also considered.

Thoughts

The question of what should be a predictor variable for style is a bit murky here. What should be fair game for predicting style and what shouldn’t? Characteristics of a beer that are defined by its style would seem to be “cheating” in a way. The only “inputs” to a beer we have in our dataset are its ingredients, primarly hops and malts. While these certainly have an effect on its flavor profile, I consider them semi-cheating because if style is determined beforehand it likely determines at least in part which ingredients are added. The main candidates in my mind are ABV, IBU, and SRM. These are “outputs” of a beer (in the sense that they can only be exactly determined once a beer is brewered) that meaningfully define it. While correlated ABV is correlated with both IBU and SRM, the three are theoretically orthogonal to each other. A style-defined attribute like glass type is a bad candidate for a predictor variable because it is completely decoupled from the beer itself and determined entirely by the style the beer has been assigned to.

The answer thus far seems to be that the beer landscape is more of a spectrum than a collection of neatly differentiated styles. Beer-intrinsic attributes like bitterness aren’t great predictors of style. The relative importance of different variables depeends on the prediction method used. However, one style-defined attribute, the glass a beer is served in, increased the accuracy of prediction substantially.

Of course, other important aspects of the flavor, body, smell, etc. of the beers could not be considered because this data is not available from BreweryDB.


source("./read_from_db.R")

Get and Prepare Data

GETting beer, the age-old dilemma

base_url <- "http://api.brewerydb.com/v2"
key_preface <- "/?key="

paginated_request <- function(ep, addition, trace_progress = TRUE) {    
  full_request <- NULL
  first_page <- fromJSON(paste0(base_url, "/", ep, "/", key_preface, key
                                , "&p=1"))
  number_of_pages <- ifelse(!(is.null(first_page$numberOfPages)), 
                            first_page$numberOfPages, 1)      

    for (page in 1:number_of_pages) {                               
    this_request <- fromJSON(paste0(base_url, "/", ep, "/", key_preface, key
                                    , "&p=", page, addition),
                             flatten = TRUE) 
    this_req_unnested <- unnest_it(this_request)    #  <- request unnested here
    if(trace_progress == TRUE) {message(paste0("Page ", this_req_unnested$currentPage))}
    full_request <- bind_rows(full_request, this_req_unnested[["data"]])
  }
  return(full_request)
} 

all_beer_raw <- paginated_request("beers", "&withIngredients=Y")
unnest_it <- function(df) {
  unnested <- df
  for(col in seq_along(df[["data"]])) {
    if(! is.null(ncol(df[["data"]][[col]]))) {
      if(! is.null(df[["data"]][[col]][["name"]])) {
        unnested[["data"]][[col]] <- df[["data"]][[col]][["name"]]
      } else {
        unnested[["data"]][[col]] <- df[["data"]][[col]][[1]]
      }
    }
  }
  return(unnested)
}

Collapse Styles

A list of our main styles to collapse to:

keywords <- c("Lager", "Pale Ale", "India Pale Ale", "Double India Pale Ale", "India Pale Lager", "Hefeweizen", "Barrel-Aged","Wheat", "Pilsner", "Pilsener", "Amber", "Golden", "Blonde", "Brown", "Black", "Stout", "Porter", "Red", "Sour", "Kölsch", "Tripel", "Bitter", "Saison", "Strong Ale", "Barley Wine", "Dubbel", "Altbier")
keyword_df <- as_tibble(list(`Main Styles` = keywords))
kable(keyword_df)
Main Styles
Lager
Pale Ale
India Pale Ale
Double India Pale Ale
India Pale Lager
Hefeweizen
Barrel-Aged
Wheat
Pilsner
Pilsener
Amber
Golden
Blonde
Brown
Black
Stout
Porter
Red
Sour
Kölsch
Tripel
Bitter
Saison
Strong Ale
Barley Wine
Dubbel
Altbier
collapse_styles <- function(df, trace_progress = TRUE) {
  
  df[["style_collapsed"]] <- vector(length = nrow(df))
  
  for (beer in 1:nrow(df)) {
    if (grepl(paste(keywords, collapse="|"), df$style[beer])) {    
      for (keyword in keywords) {         
        if(grepl(keyword, df$style[beer]) == TRUE) {
          df$style_collapsed[beer] <- keyword    
        }                         
      } 
    } else {
      df$style_collapsed[beer] <- as.character(df$style[beer])       
    }
    if(trace_progress == TRUE) {message(paste0("Collapsing this ", df$style[beer], " to: ", df$style_collapsed[beer]))}
  }
  return(df)
}
collapse_further <- function(df) {
  df[["style_collapsed"]] <- df[["style_collapsed"]] %>%
    fct_collapse(
      "Wheat" = c("Hefeweizen", "Wheat"),
      "Pilsener" = c("Pilsner", "American-Style Pilsener") # pilsener == pilsner == pils
    )
  return(df)
}

Split out Ingredients

When we unnested ingredients, we just concatenated all of the ingredients for a given beer into a long string. If we want, we can split out the ingredients that were concatenated in <ingredient>_name with this split_ingredients function.

This takes a vector of ingredients_to_split, so e.g. c("hops_name", "malt_name") and creates one column for each type of ingredient (hops_name_1, hops_name_2, etc.). It’s flexible enough to adapt if data in BreweryDB changes and a beer now has 15 hops where before the maximum number of hops a beer had was 10.

split_ingredients <- function(df, ingredients_to_split) {
  
  ncol_df <- ncol(df)
  
  for (ingredient in ingredients_to_split) {

    ingredient_split <- str_split(df[[ingredient]], ", ")    
    num_new_cols <- max(lengths(ingredient_split))    
  
    for (num in 1:num_new_cols) {
      
      this_col <- ncol_df + 1         
      
      df[, this_col] <- NA
      names(df)[this_col] <- paste0(ingredient, "_", num)
      ncol_df <- ncol(df)             
      for (row in seq_along(ingredient_split)) {          
        if (!is.null(ingredient_split[[row]][num])) {        
          df[row, this_col] <- ingredient_split[[row]][num]
        }
      }
      df[[names(df)[this_col]]] <- factor(df[[names(df)[this_col]]])
    }
    
    ncol_df <- ncol(df)
  }
  return(df)
}

Some quick summary stats on our main dataframe called beer_necessities:

dim(beer_necessities)
[1] 63495    39
str(beer_necessities)
'data.frame':   63495 obs. of  39 variables:
 $ id              : chr  "cBLTUw" "ZsQEJt" "tmEthz" "b7SfHG" ...
 $ name            : chr  "\"18\" Imperial IPA 2" "\"633\" American Pale Ale" "\"Admiral\" Stache" "\"Ah Me Joy\" Porter" ...
 $ description     : chr  "Hop Heads this one's for you!  Checking in with 143 IBU's this ale punches you in the mouth with extreme bitterness then rounds"| __truncated__ "Our first beer has been aptly named \"633\" after the Regions telephone exchange for starters.  \"If I could call a beer home, "| __truncated__ "Milwaukee Brewing Co’s take on a classic European style. Baltic Porters are the stronger lager fermented cousin of the classic "| __truncated__ "A robust porter style ale with a twist. This beer has moderate roastiness with a bitter finish, complemented by the sweetness o"| __truncated__ ...
 $ style           : Factor w/ 170 levels "Adambier","Aged Beer (Ale or Lager)",..: 13 20 30 139 23 108 8 20 7 54 ...
 $ abv             : num  11.1 6.33 7 5.4 4.8 4.6 8.5 5.8 7.6 10.8 ...
 $ ibu             : num  NA 25 23 51 12 NA 30 51 80 70 ...
 $ srm             : num  33 NA 37 40 NA 5 NA 8 NA NA ...
 $ glass           : Factor w/ 12 levels "Flute","Goblet",..: 6 NA 6 NA NA NA NA NA NA NA ...
 $ hops_name       : Factor w/ 969 levels "","#06300","Admiral, Aurora, Challenger, Fuggle (American), Target",..: NA NA 902 NA NA NA NA NA NA NA ...
 $ hops_id         : Factor w/ 969 levels "","1, 22, 23, 25, 146",..: NA NA 955 NA NA NA NA NA NA NA ...
 $ malt_name       : Factor w/ 923 levels "","Abbey Malt",..: NA NA 143 NA NA NA NA NA NA NA ...
 $ malt_id         : Factor w/ 923 levels "","165","165, 1947, 633, 1922",..: NA NA 124 NA NA NA NA NA NA NA ...
 $ glasswareId     : num  5 NA 5 NA NA NA NA NA NA NA ...
 $ styleId         : Factor w/ 170 levels "1","10","100",..: 109 89 7 82 106 101 102 89 107 68 ...
 $ style.categoryId: num  3 3 9 1 3 3 3 3 3 1 ...
 $ style_collapsed : Factor w/ 108 levels "Adambier","Altbier",..: 101 81 84 84 95 21 24 81 20 101 ...
 $ hops_name_1     : Factor w/ 113 levels "","#06300","Admiral",..: NA NA 90 NA NA NA NA NA NA NA ...
 $ hops_name_2     : Factor w/ 98 levels "Amarillo","Apollo",..: NA NA 70 NA NA NA NA NA NA NA ...
 $ hops_name_3     : Factor w/ 90 levels "Amarillo","Aramis",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_4     : Factor w/ 63 levels "Amarillo","Apollo",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_5     : Factor w/ 50 levels "Australian Dr. Rudi",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_6     : Factor w/ 29 levels "Cascade","Columbus",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_7     : Factor w/ 20 levels "Azzeca","Crystal",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_8     : Factor w/ 10 levels "Equinox","Experimental 06277",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_9     : Factor w/ 4 levels "Motueka","Nugget",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_10    : Factor w/ 4 levels "Nelson Sauvin",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_11    : Factor w/ 4 levels "Simcoe","Styrian Bobeks",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_12    : Factor w/ 3 levels "Summit","Super Galena",..: NA NA NA NA NA NA NA NA NA NA ...
 $ hops_name_13    : Factor w/ 2 levels "Target","Willamette": NA NA NA NA NA NA NA NA NA NA ...
 $ malt_name_1     : Factor w/ 128 levels "","Abbey Malt",..: NA NA 9 NA NA NA NA NA NA NA ...
 $ malt_name_2     : Factor w/ 134 levels "Amber Malt","Barley - Flaked",..: NA NA 64 NA NA NA NA NA NA NA ...
 $ malt_name_3     : Factor w/ 122 levels "Acidulated Malt",..: NA NA 73 NA NA NA NA NA NA NA ...
 $ malt_name_4     : Factor w/ 94 levels "Bamberg Smoked Malt",..: NA NA 52 NA NA NA NA NA NA NA ...
 $ malt_name_5     : Factor w/ 62 levels "Asheburne Mild Malt",..: NA NA NA NA NA NA NA NA NA NA ...
 $ malt_name_6     : Factor w/ 41 levels "Aromatic Malt",..: NA NA NA NA NA NA NA NA NA NA ...
 $ malt_name_7     : Factor w/ 19 levels "Barley - Roasted",..: NA NA NA NA NA NA NA NA NA NA ...
 $ malt_name_8     : Factor w/ 15 levels "Crisp 120","Crisp 77",..: NA NA NA NA NA NA NA NA NA NA ...
 $ malt_name_9     : Factor w/ 6 levels "Rye Malt","Smoked Malt",..: NA NA NA NA NA NA NA NA NA NA ...
 $ malt_name_10    : Factor w/ 1 level "Victory Malt": NA NA NA NA NA NA NA NA NA NA ...

Find the Most Popualar Styles

We find mean ABV, IBU, and SRM per collapsed style and arrange collapsed styles by the number of beers that fall into them. (This is of course dependent on how we collapse styles; if we looped all Double IPAs in with IPAs then the category IPA would be much bigger than it is if we keep the two separate.)

library(forcats)
# Pare down to only cases where style is not NA
beer_dat_pared <- beer_necessities[complete.cases(beer_necessities$style), ]
# Arrange beer dat by style popularity
style_popularity <- beer_dat_pared %>% 
  group_by(style) %>% 
  count() %>% 
  arrange(desc(n))
# Add a column that scales popularity
style_popularity <- bind_cols(style_popularity, 
                               n_scaled = as.vector(scale(style_popularity$n)))
# Find styles that are above a z-score of 0
popular_styles <- style_popularity %>% 
  filter(n_scaled > 0)
# Pare dat down to only beers that fall into those styles
popular_beer_dat <- beer_dat_pared %>% 
  filter(
    style %in% popular_styles$style
  ) %>% 
  droplevels() %>% 
  as_tibble() 

How many rows do we have in our dataset of just beers that fall into the popular styles?

nrow(popular_beer_dat)
[1] 45871

Now we find the style centers.

# Find the centers (mean abv, ibu, srm) of the most popular styles
style_centers <- popular_beer_dat %>% 
  group_by(style_collapsed) %>% 
  add_count() %>% 
  summarise(
    mean_abv = mean(abv, na.rm = TRUE),
    mean_ibu = mean(ibu, na.rm = TRUE), 
    mean_srm = mean(srm, na.rm = TRUE),
    n = median(n, na.rm = TRUE)          # Median here only for summarise. Should be just the same as n
  ) %>% 
  arrange(desc(n)) %>% 
  drop_na() %>% 
  droplevels()
# Give some nicer names
style_centers_rename <- style_centers %>% 
  rename(
    `Collapsed Style` = style_collapsed,
    `Mean ABV` = mean_abv,
    `Mean IBU` = mean_ibu,
    `Mean SRM` = mean_srm,
    `Numer of Beers` = n
  )

Take a look at the table, ordered by number of beers in that style, descending.

kable(style_centers_rename)
Collapsed Style Mean ABV Mean IBU Mean SRM Numer of Beers
India Pale Ale 6.578468 66.04268 9.989313 6524
Pale Ale 5.695480 40.86930 8.890306 4280
Stout 7.991841 43.89729 36.300000 4238
Wheat 5.158040 17.47168 5.861842 3349
Double India Pale Ale 8.930599 93.48142 11.006873 2525
Red 5.742565 33.81127 16.178862 2521
Lager 5.453718 30.64361 8.457447 2230
Saison 6.400189 27.25114 7.053476 2167
Blonde 5.595298 22.39432 5.625000 2044
Porter 6.182049 33.25369 32.197605 1973
Brown 6.159212 32.21577 23.592000 1462
Pilsener 5.227593 33.51346 4.413462 1268
Specialty Beer 6.446402 33.77676 15.520548 1044
Bitter 5.322364 38.28175 12.460526 939
Fruit Beer 5.195222 19.24049 8.666667 905
Herb and Spice Beer 6.621446 27.77342 18.166667 872
Sour 6.224316 18.88869 10.040816 797
Strong Ale 8.826425 36.74233 22.547945 767
Tripel 9.029775 32.51500 7.680556 734
Black 6.958714 65.50831 31.080000 622
Barley Wine 10.781600 74.04843 19.561404 605
Kölsch 4.982216 23.37183 4.371795 593
Barrel-Aged 9.002506 39.15789 18.133333 540
Other Belgian-Style Ales 7.516318 37.55812 17.549020 506
Pumpkin Beer 6.712839 23.48359 17.918033 458
Dubbel 7.509088 25.05128 22.940000 399
Scotch Ale 7.620233 26.36909 24.222222 393
German-Style Doppelbock 8.045762 28.88692 25.696970 376
Fruit Cider 6.205786 25.60000 12.000000 370
German-Style Märzen 5.746102 25.63796 14.322581 370

Ingredients

The lifecycle of ingredients thus far has been to unnest them from the raw JSON into a long string contained in hops_name and malt_name. Next each ingredient in each of those columns was split out into hops_name_1, hops_name_2, etc.

To get more granular with ingredients, we can further split out each individual ingredient into its own column. If a beer or style contains that ingredient, its row gets a 1 in that ingredient column and a 0 otherwise.

From this, we can find the total number of hops and malts per grouper.

  • This function takes a dataframe and two other parameters set at the outset:
    • ingredient_want: this can be hops, malt, or other ingredients like yeast if we pull that in
    • grouper: can be a vector of one or more things to group by, like beer id or style
    • Careful with using name as a grouper as multiple beers have the same name
pick_ingredient_get_beer <- function (ingredient_want, df, grouper) {
  
  # ----------------------- Setup --------------------------- #
  # We've already split ingredient number names out from the concatenated string into columns like `malt_name_1`,
  # `malt_name_2`, etc. We need to find the range of these columns; there will be a different number of malt
  # columns than hops columns, for instance. The first one will be `<ingredient>_name_1` and from this we can find
  # the index of this column in our dataframe. We get the name of last one with the `get_last_ing_name_col()`
  # function. Then we save a vector of all the ingredient column names in `ingredient_colnames`. It will stay
  # constant even if the indices change when we select out certain columns. 
  
  # First ingredient
  first_ingredient_name <- paste(ingredient_want, "_name_1", sep="")
  first_ingredient_index <- which(colnames(df)==first_ingredient_name)
  
  # Get the last ingredient
  get_last_ing_name_col <- function(df) {
    for (col in names(df)) {
      if (grepl(paste(ingredient_want, "_name_", sep = ""), col) == TRUE) {
        name_last_ing_col <- col
      }
    }
    return(name_last_ing_col)
  }
  
  # Last ingredient
  last_ingredient_name <- get_last_ing_name_col(df)
  last_ingredient_index <- which(colnames(df)==last_ingredient_name)
  
  # Vector of all the ingredient column names
  ingredient_colnames <- names(df)[first_ingredient_index:last_ingredient_index]
  
  # Non-ingredient column names we want to keep
  to_keep_col_names <- c("id", "cluster_assignment", "name", "abv", "ibu", "srm", "style", "style_collapsed")
  
  # -------------------------------------------------------------------------------# 
  
  # Inside `gather_ingredients()` we take out superflous column names that are not in `to_keep_col_names` or one 
  # of the ingredient columns, find what the new ingredient column indices are, since they'll have changed after 
  # we pared down and then gather all of the ingredient columns (e.g., `hops_name_1`) into one long column, 
  # `ing_keys` and all the actual ingredient names (e.g., Cascade) into `ing_names`.
  
  # ----------------------------- Gather columns --------------------------------- #
  gather_ingredients <- function(df, cols_to_gather) {
    to_keep_indices <- which(colnames(df) %in% to_keep_col_names)
    
    selected_df <- df[, c(to_keep_indices, first_ingredient_index:last_ingredient_index)]
    
    new_ing_indices <- which(colnames(selected_df) %in% cols_to_gather)    # indices will have changed since we pared down 
    
    df_gathered <- selected_df %>%
      gather_(
        key_col = "ing_keys",
        value_col = "ing_names",
        gather_cols = colnames(selected_df)[new_ing_indices]
      ) %>%
      mutate(
        count = 1
      )
    return(df_gathered)
  }
  beer_gathered <- gather_ingredients(df, ingredient_colnames)  # ingredient colnames defined above function
  # ------------------------------------------------------------------------------- # 
  
  # Next we get a vector of all ingredient levels and take out the one that's an empty string and 
  # use this vector of ingredient levels in `select_spread_cols()` below
  # Get a vector of all ingredient levels
  beer_gathered$ing_names <- factor(beer_gathered$ing_names)
  ingredient_levels <- levels(beer_gathered$ing_names) 
  
  # Take out the level that's just an empty string
  to_keep_levels <- !(c(1:length(ingredient_levels)) %in% which(ingredient_levels == ""))
  ingredient_levels <- ingredient_levels[to_keep_levels]
  
  beer_gathered$ing_names <- as.character(beer_gathered$ing_names)
  
  # ----------------------------------------------------------------------------- # 
  
  # Then we spread the ingredient names: we take what was previously the `value` in our gathered dataframe, the
  # actual ingredient names (Cascade, Centennial) and make that our `key`; it'll form the new column names. The
  # new `value` is `value` is count; it'll populate the row cells. If a given row has a certain ingredient, it
  # gets a 1 in the corresponding cell, an NA otherwise. 
  # We add a unique idenfitier for each row with `row`, which we'll drop later (see [Hadley's SO
  # comment](https://stackoverflow.com/questions/25960394/unexpected-behavior-with-tidyr)).
  
  # ------------------------------- Spread columns -------------------------------- #
  spread_ingredients <- function(df) {
    df_spread <- df %>% 
      mutate(
        row = 1:nrow(df)        # Add a unique idenfitier for each row which we'll need in order to spread; we'll drop this later
      ) %>%                                 
      spread(
        key = ing_names,
        value = count
      ) 
    return(df_spread)
  }
  beer_spread <- spread_ingredients(beer_gathered)
  # ------------------------------------------------------------------------------- # 
  
  # ------------------------- Select only certain columns ------------------------- #
  select_spread_cols <- function(df) {
    to_keep_col_indices <- which(colnames(df) %in% to_keep_col_names)
    to_keep_ingredient_indices <- which(colnames(df) %in% ingredient_levels)
    
    to_keep_inds_all <- c(to_keep_col_indices, to_keep_ingredient_indices)
    
    new_df <- df %>% 
      select_(
        .dots = to_keep_inds_all
      )
    return(new_df)
  }
  beer_spread_selected <- select_spread_cols(beer_spread)
  # ------------------------------------------------------------------------------- # 
  # Take out all rows that have no ingredients specified at all
  inds_to_remove <- apply(beer_spread_selected[, first_ingredient_index:last_ingredient_index], 
                          1, function(x) all(is.na(x)))
  beer_spread_no_na <- beer_spread_selected[ !inds_to_remove, ]
  
  
  # ----------------- Group ingredients by the grouper specified ------------------- #
  # Then we do the final step and group by the groupers.
  
  get_ingredients_per_grouper <- function(df, grouper = grouper) {
    df_grouped <- df %>%
      ungroup() %>% 
      group_by_(grouper)
    
    not_for_summing <- which(colnames(df_grouped) %in% to_keep_col_names)
    max_not_for_summing <- max(not_for_summing)
    
    per_grouper <- df_grouped %>% 
      select(-c(abv, ibu, srm)) %>%    # taking out temporarily
      summarise_if(
        is.numeric,              
        sum, na.rm = TRUE
      ) %>%
      mutate(
        total = rowSums(.[(max_not_for_summing + 1):ncol(.)], na.rm = TRUE)    
      )
    
    # Send total to the second position
    per_grouper <- per_grouper %>% 
      select(
        id, total, everything()
      )
    
    # Replace total column with more descriptive name: total_<ingredient>
    names(per_grouper)[which(names(per_grouper) == "total")] <- paste0("total_", ingredient_want)
    
    return(per_grouper)
  }
  # ------------------------------------------------------------------------------- # 
  
  ingredients_per_grouper <- get_ingredients_per_grouper(beer_spread_selected, grouper)
  return(ingredients_per_grouper)
}

Now run the function with ingredient_want as first hops, and then malt. Then join the resulting dataframes and remove/reorder some columns.

# Run the entire function with ingredient_want set to hops, grouping by name
ingredients_per_beer_hops <- pick_ingredient_get_beer(ingredient_want = "hops", 
                                                      beer_necessities, 
                                                      grouper = c("id"))
# Same for malt
ingredients_per_beer_malt <- pick_ingredient_get_beer(ingredient_want = "malt", 
                                                      beer_necessities, 
                                                      grouper = c("id"))
# Join those on our original dataframe by name
beer_ingredients_join_first_ingredient <- left_join(beer_necessities, ingredients_per_beer_hops,
                                                    by = "id")
beer_ingredients_join_all <- left_join(beer_ingredients_join_first_ingredient, ingredients_per_beer_malt,
                                   by = "id")
# Take out some unnecessary columns
unnecessary_cols <- c("styleId", "abv_scaled", "ibu_scaled", "srm_scaled", 
                      "hops_id", "malt_id", "glasswareId", "style.categoryId")
beer_ingredients_join_all <- beer_ingredients_join_all[, (! names(beer_ingredients_join_all) %in% unnecessary_cols)]
# If we also want to take out any of the malt_name_1, malt_name_2, etc. columns we can do this with a grep
more_unnecessary <- c("hops_name_|malt_name_")
beer_ingredients_join_all <- 
  beer_ingredients_join_all[, (! grepl(more_unnecessary, names(beer_ingredients_join_all)) == TRUE)]
# Reorder columns a bit
beer_ingredients_join_all <- beer_ingredients_join_all %>% 
  select(
    id, name, total_hops, total_malt, everything(), -description
  )
# Keep only beers that fall into a style_collapsed bucket
# We're not filtering by levels in beer_necessities$style_collapsed because those levels contain more than what's in just the keywords of collapse_styles()
beer_ingredients_join <- beer_ingredients_join_all %>% 
  filter(
    style_collapsed %in% levels(style_centers$style_collapsed)
  ) %>% 
  droplevels()
# And get a df that includes total_hops and total_malt but not all the other ingredient columns
beer_totals_all <- beer_ingredients_join_all %>% 
  select(
    id, name, total_hops, total_malt, style, style_collapsed,
    abv, ibu, srm, glass, hops_name, malt_name
  )
# And just styles in style_collapsed
beer_totals <- beer_ingredients_join %>% 
  filter(
    style_collapsed %in% levels(style_centers$style_collapsed)
  ) %>% 
  droplevels()

Now we’re left with something of a sparse matrix of all the ingredients compared to all the beers

kable(beer_ingredients_join[1:20, ])
id name total_hops total_malt style abv ibu srm glass hops_name malt_name style_collapsed #06300 Admiral Aged / Debittered Hops (Lambic) Ahtanum Alchemy Amarillo Amarillo Gold Apollo Aquila Aramis Argentine Cascade Athanum Aurora Australian Dr. Rudi Azacca Azzeca Belma Bobek Bramling Cross Bravo Brewer's Gold Brewer's Gold (American) Calypso Cascade Celeia Centennial Challenger Chinook Citra Cluster Cobb Columbus Columbus (Tomahawk) Comet Crystal CTZ Delta East Kent Golding El Dorado Ella Enigma Equinox Eureka Experimental 05256 Experimental 06277 Falconer's Flight First Gold French Strisserspalt French Triskel Fuggle (American) Fuggle (English) Fuggles Galaxy Galena German Magnum German Mandarina Bavaria German Opal German Perle German Polaris German Select German Tradition Glacier Golding (American) Green Bullet Hallertau Hallertauer Mittelfrüher Hallertau Hallertauer Tradition Hallertau Northern Brewer Hallertauer (American) Hallertauer Herkules Hallertauer Hersbrucker Hallertauer Perle Hallertauer Select Helga Hop Extract Hops Horizon Huell Melon Idaho 7 Jarrylo Kent Goldings Kohatu Lemon Drop Liberty Magnum Marynka Meridian Millenium Mosaic Motueka Mount Hood Mt. Rainier Nelson Sauvin New Zealand Hallertauer New Zealand Motueka New Zealand Sauvin Newport Noble Northdown Northern Brewer (American) Nugget Orbit Pacific Gem Pacific Jade Pacifica Palisades Perle (American) Phoenix Pilgrim Premiant Pride of Ringwood Rakau Revolution Saaz (American) Saaz (Czech) Santiam Saphir (German Organic) Simcoe Sladek (Saaz) Sorachi Ace Southern Cross Sovereign Spalt Spalt Select Spalt Spalter Sterling Sticklebract Strisselspalt Styrian Aurora Styrian Bobeks Styrian Goldings Summit Super Galena Target Tettnang Tettnanger Tettnanger (American) Tomahawk Topaz Tradition Ultra Vanguard Vic Secret Waimea Wakatu Warrior Willamette Yakima Willamette Zeus Zythos Abbey Malt Acidulated Malt Amber Malt Aromatic Malt Asheburne Mild Malt Bamberg Smoked Malt Barley - Black Barley - Flaked Barley - Lightly Roasted Barley - Malted Barley - Raw Barley - Roasted Barley - Roasted/De-husked Beechwood Smoked Belgian Pale Belgian Pilsner Biscuit Malt Black Malt Black Malt - Debittered Black Malt - Organic Black Patent Black Roast Blackprinz Malt Blue Agave Nectar Blue Corn Bonlander Briess 2-row Chocolate Malt Briess Blackprinz Malt British Pale Malt Brown Malt Brown Sugar Buckwheat - Roasted C-15 Canada 2-Row Silo Cane Sugar Cara Malt CaraAmber CaraAroma CaraBrown Carafa I Carafa II Carafa III Carafa Special CaraFoam CaraHell Caramel/Crystal Malt Caramel/Crystal Malt - Dark Caramel/Crystal Malt - Extra Dark Caramel/Crystal Malt - Heritage Caramel/Crystal Malt - Light Caramel/Crystal Malt - Medium Caramel/Crystal Malt - Organic Caramel/Crystal Malt 10L Caramel/Crystal Malt 120L Caramel/Crystal Malt 150L Caramel/Crystal Malt 15L Caramel/Crystal Malt 20L Caramel/Crystal Malt 300L Caramel/Crystal Malt 30L Caramel/Crystal Malt 40L Caramel/Crystal Malt 45L Caramel/Crystal Malt 50L Caramel/Crystal Malt 55L Caramel/Crystal Malt 60L Caramel/Crystal Malt 70L Caramel/Crystal Malt 75L Caramel/Crystal Malt 80L Caramel/Crystal Malt 85L Caramel/Crystal Malt 8L Caramel/Crystal Malt 90L CaraMunich CaraMunich 120L CaraMunich 20L CaraMunich 40L CaraMunich 60L CaraMunich I CaraMunich II CaraMunich III CaraPils/Dextrin Malt CaraRed CaraRye CaraStan CaraVienne Malt CaraWheat Carolina Rye Malt Cereal Cherry Smoked Cherrywood Smoke Malt Chit Malt Chocolate Malt Chocolate Rye Malt Chocolate Wheat Malt Coffee Malt Corn Corn - Field Corn - Flaked Corn Grits Crisp 120 Crisp 77 Crystal 77 Dark Chocolate Dememera Sugar Dextrin Malt Dextrose Syrup Extra Special Malt Fawcett Crystal Rye Fawcett Rye German Cologne Gladfield Pale Glen Eagle Maris Otter Golden Promise Harrington 2-Row Base Malt High Fructose Corn Syrup Honey Honey Malt Hugh Baird Pale Ale Malt Kiln Amber Lactose Lager Malt Malt Extract Malted Rye Malto Franco-Belge Pils Malt Maple Syrup Maris Otter Melanoidin Malt Metcalfe Midnight Wheat Mild Malt Millet Munich Malt Munich Malt - Dark Munich Malt - Light Munich Malt - Organic Munich Malt - Smoked Munich Malt - Type I Munich Malt - Type II Munich Malt 10L Munich Malt 20L Munich Malt 40L Munich Wheat Oats - Flaked Oats - Golden Naked Oats - Malted Oats - Rolled Oats - Steel Cut (Pinhead Oats) Oats - Toasted Pale Chocolate Malt Pale Malt Pale Malt - Halcyon Pale Malt - Optic Pale Malt - Organic Pale Wheat Palev Pearl Malt Peated Malt - Smoked Piloncillo Pilsner Malt Pilsner Malt - Organic Rahr 2-Row Malt Rahr Special Pale Rauchmalz Rice Rice - Flaked Rice - Hulls Rice - Red Rice - White Roast Malt Rye - Flaked Rye Malt Samuel Adams two-row pale malt blend Six-Row Pale Malt Smoked Malt Special B Malt Special Roast Special W Malt Spelt Malt Sugar (Albion) Toasted Malt Torrefied Wheat Two-Row Barley Malt Two-Row Pale Malt Two-Row Pale Malt - Organic Two-Row Pale Malt - Toasted Two-Row Pilsner Malt Two-Row Pilsner Malt - Belgian Two-Row Pilsner Malt - Germany Victory Malt Vienna Malt Weyermann Rye Wheat - Flaked Wheat - Raw Wheat - Red Wheat - Toasted Wheat - Torrified Wheat Malt Wheat Malt - Dark Wheat Malt - German Wheat Malt - Light Wheat Malt - Organic Wheat Malt - Red Wheat Malt - Smoked Wheat Malt - White White Wheat Wyermann Vienna
cBLTUw "18" Imperial IPA 2 0 0 American-Style Imperial Stout 11.10 NA 33 Pint NA NA Stout 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ZsQEJt "633" American Pale Ale 0 0 American-Style Pale Ale 6.33 25.0 NA NA NA NA Pale Ale 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
tmEthz "Admiral" Stache 2 4 Baltic-Style Porter 7.00 23.0 37 Pint Perle (American), Saaz (American) Barley - Malted, Chocolate Malt, Munich Malt, Oats - Flaked Porter 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
b7SfHG "Ah Me Joy" Porter 0 0 Robust Porter 5.40 51.0 40 NA NA NA Porter 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
zcJMId "Alternating Currant" Sour 0 0 American-Style Sour Ale 4.80 12.0 NA NA NA NA Sour 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
UM8GL6 "B" Street Pineapple Blonde 0 0 Golden or Blonde Ale 4.60 NA 5 NA NA NA Blonde 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
NIaY9C "B.B. Rodriguez" Double Brown 0 0 American-Style Brown Ale 8.50 30.0 NA NA NA NA Brown 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
PBEXhV "Bison Eye Rye" Pale Ale | 2 of 4 Part Pale Ale Series 0 0 American-Style Pale Ale 5.80 51.0 8 NA NA NA Pale Ale 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
wRmmdv "California Crude" Black IPA 0 0 American-Style Black Ale 7.60 80.0 NA NA NA NA Black 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
EPYNpW "C’est Noir" Imperial Stout 0 0 British-Style Imperial Stout 10.80 70.0 NA NA NA NA Stout 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
AXmvOd "Dust Up" Cloudy Pale Ale | 1 of 4 Part Pale Ale Series 0 0 American-Style Pale Ale 5.40 54.0 11 NA NA NA Pale Ale 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
c5pZg5 "EVL1" Imperial Red Ale 0 0 Imperial Red Ale 10.40 64.0 NA NA NA NA Red 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
xBKAka "Galactic Wrath" IPA 0 0 American-Style India Pale Ale 7.50 75.0 NA NA NA NA India Pale Ale 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Hr5A0t "God Country" Kolsch 0 0 German-Style Kölsch / Köln-Style Kölsch 5.60 28.2 5 NA NA NA Kölsch 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
UjFyXJ "Hey Victor" Smoked Porter 0 0 Smoke Beer (Lager or Ale) 5.50 NA NA NA NA NA Lager 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
5UcMBc "Ignition" IPA 0 0 American-Style India Pale Ale 6.60 45.0 NA Pint NA NA India Pale Ale 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
mrVjY4 "Jemez Field Notes" Golden Lager 0 0 Golden or Blonde Ale 4.90 20.0 5 NA NA NA Blonde 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
54rSgo "Jemmy Dean" Breakfast Stout 0 0 Sweet or Cream Stout NA NA NA Pint NA NA Stout 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
JsKjkk "Mauvaises Choses" 0 0 Belgian-Style Pale Strong Ale 7.00 30.0 NA Tulip NA NA Strong Ale 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
b7WWL6 "Mike Saw a Sasquatch" Session Ale 2 2 Golden or Blonde Ale 4.70 26.0 NA Pint Cascade, Sterling Honey Malt, Two-Row Pale Malt Blonde 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Now that the munging is done, onto the main question: do natural clusters in beer align with style boundaries?


Unsupervised Clustering

We K-means cluster beers based on certain numeric predictor variables.

Prep

  • Write a funciton that takes a dataframe, a set of predictors, a response variable, and the number of cluster centers you want
    • NB: There are not not very many beers have SRM so we may not want to omit based on it
  • Take out missing values, and scale the data
  • Take out outliers, defined as beers have to have an ABV between 3 and 20 and an IBU less than 200
  • Then cluster on just the predictors and compare to the response variable
library(NbClust)
prep_clusters <- function(df, preds, to_scale, resp) {
  df_for_clustering <- df %>%
    select_(.dots = c(response_vars, cluster_on)) %>%
    na.omit() %>%
    filter(
      abv < 20 & abv > 3    # Only keep beers with ABV between 3 and 20 and an IBU less than 200
    ) %>%
    filter(
      ibu < 200    
    )
  
  df_all_preds <- df_for_clustering %>%
    select_(.dots = preds)
  
  df_preds_scale <- df_all_preds %>%
    select_(.dots = to_scale) %>%
    rename(
      abv_scaled = abv,
      ibu_scaled = ibu,
      srm_scaled = srm
    ) %>%
    scale() %>%
    as_tibble()
  
  df_preds <- bind_cols(df_preds_scale, df_all_preds[, (!names(df_all_preds) %in% to_scale)])
  
  df_outcome <- df_for_clustering %>%
    select_(.dots = resp) %>%
    na.omit()
  
  cluster_prep_out <- list(df_for_clustering = df_for_clustering, preds = df_preds, outcome = df_outcome)
  
  return(cluster_prep_out)
}

Prep the clusters. We’ll cluster on the predictors ABV, IBU, SRM, total number of hops, and total number of malts.

cluster_on <- c("abv", "ibu", "srm", "total_hops", "total_malt")
to_scale <- c("abv", "ibu", "srm", "total_hops", "total_malt")
response_vars <- c("name", "style", "style_collapsed")
cluster_prep <- prep_clusters(df = beer_totals,
                   preds = cluster_on,
                   to_scale = to_scale,
                   resp = response_vars)

Determine an optimal number of clusters by setting the minimum to 2 and max to 15 clusters. From the resulting histogram, 10 seemed an optimal number of clusters.

nb <- NbClust(cluster_prep$preds, distance = "euclidean",
              min.nc = 2, max.nc = 15, method = "kmeans")
hist(nb$Best.nc[1,], breaks = max(na.omit(nb$Best.nc[1,])))

Cluster

Now cluster on the preped predictors with 10 centers.

cluster_it <- function(df_preds, n_centers) {
  set.seed(9)
  clustered_df_out <- kmeans(x = df_preds$preds, centers = n_centers, trace = FALSE)
  clustered_df <- as_tibble(data.frame(
    cluster_assignment = factor(clustered_df_out$cluster),
    df_preds$outcome, df_preds$preds,
    df_preds$df_for_clustering %>% select(abv, ibu, srm)))
  return(clustered_df)
}
clustered_beer <- cluster_it(df_preds = cluster_prep, n_centers = 10)

Head of the resulting clustered data. Cluster assignment column on the far left.

kable(clustered_beer[1:20, ])

# How many rows do we have?
nrow(clustered_beer)

Join the clustered beer on beer_ingredients_join

beer_ingredients_join_clustered <- left_join(beer_ingredients_join, clustered_beer, 
                                             by = "name")

A table of cluster counts broken down by style

cluster_table_counts <- table(style = clustered_beer$style_collapsed, cluster = clustered_beer$cluster_assignment)

kable(cluster_table_counts)

Plot the clusters. There are 3 axes: ABV, IBU, and SRM, so we choose two at a time.

clustered_beer_plot_abv_ibu <- ggplot(data = clustered_beer, aes(x = abv, y = ibu, colour = cluster_assignment)) + 
  geom_jitter() + theme_minimal()  +
  ggtitle("k-Means Clustering of Beer by ABV, IBU, SRM") +
  labs(x = "ABV", y = "IBU") +
  labs(colour = "Cluster Assignment")
clustered_beer_plot_abv_ibu

clustered_beer_plot_abv_srm <- ggplot(data = clustered_beer, aes(x = abv, y = srm, colour = cluster_assignment)) + 
  geom_jitter() + theme_minimal()  +
  ggtitle("k-Means Clustering of Beer by ABV, IBU, SRM") +
  labs(x = "ABV", y = "SRM") +
  labs(colour = "Cluster Assignment")
clustered_beer_plot_abv_srm

Now we can add in the style centers (means) for each style_collapsed and label it.

library(ggrepel)
abv_ibu_clusters_vs_style_centers <- ggplot() +   
  geom_point(data = clustered_beer, 
             aes(x = abv, y = ibu, colour = cluster_assignment), alpha = 0.5) +
  geom_point(data = style_centers,
             aes(mean_abv, mean_ibu), colour = "black") +
  geom_text_repel(data = style_centers, aes(mean_abv, mean_ibu, label = style_collapsed), 
                  box.padding = unit(0.45, "lines"),
                  family = "Calibri",
                  label.size = 0.3) +
  ggtitle("Popular Styles vs. k-Means Clustering of Beer by ABV, IBU, SRM") +
  labs(x = "ABV", y = "IBU") +
  labs(colour = "Cluster Assignment") +
  theme_bw()
abv_ibu_clusters_vs_style_centers

The clustering above used a smaller number of clusters (10) than there are styles_collapsed. That makes it difficult to determine whether a given style fits snugly into a cluster or not.

Cluster on just certain selected styles

We’ll take five very distinct collapsed styles and re-run the clustering on beers that fall into these categories. These styles were intentionally chosen because they are quite distinct: Blonde, IPA, Stout, Tripel, Wheat. Arguably, of these five styles Blondes and Wheats are the closest

styles_to_keep <- c("Blonde", "India Pale Ale", "Stout", "Tripel", "Wheat")
bt_certain_styles <- beer_totals %>%
  filter(
    style_collapsed %in% styles_to_keep
  ) %>% 
  droplevels()
cluster_on <- c("abv", "ibu", "srm", "total_hops", "total_malt")
to_scale <- c("abv", "ibu", "srm", "total_hops", "total_malt")
response_vars <- c("name", "style", "style_collapsed")
bt_cluster_prep <- prep_clusters(df = bt_certain_styles,
                   preds = cluster_on,
                   to_scale = to_scale,
                   resp = response_vars)
certain_styles_clustered <- cluster_it(df_preds = bt_cluster_prep, n_centers = 5)
style_centers_certain_styles <- style_centers %>% 
  filter(style_collapsed %in% styles_to_keep)

Table of style vs. cluster.

kable(table(style = certain_styles_clustered$style_collapsed, cluster = certain_styles_clustered$cluster_assignment))

Now that we have a manageable number of styles, we can see how well fit each cluster is to each style. If the features we clustered on perfectly predicted style, there would each color (cluster) would be unique to each facet of the plot. (E.g., left entirely blue, second from left entirely green, etc.)

Random asides into hops

Do more hops always mean more bitterness?

Regressing total number of hops on bitterness (IBU):

This holds even with 5 or more hops.

Most popular hops

Are there certian hops that are used more often in very high IBU or ABV beers? Hard to detect a pattern

Neural Net

We’ll set the dataframe to be beer_totals, the predictor variables to be the vector contained in p_vars, the outcome to be style_collapsed

And now if we add glass as a predictor?

Random forest with all ingredients

  • We can use a random forest to get even more granular with ingredients
    • The sparse ingredient dataframe was too complex for the multinomial neural net but the ranger can handle sparse data like this
  • Here we don’t include glass as a predictor

OOB (out of bag) prediction error is calculated from tree samples constructed but not used in training set; these trees become effectively part of test set.

We can compare predicted classification on the test set to their actual style classification.

Variable importance

  • Interestingly, ABV, IBU, and SRM are all much more important in the random forest than total_hops and total_malt

How does a CSRF (case-specific random forest) fare?


bi_csrf <- csrf(style_collapsed ~ ., training_data = bi_train, test_data = bi_test,
                params1 = list(num.trees = 5, mtry = 4),
                params2 = list(num.trees = 2))

csrf_acc <- postResample(bi_csrf, bi_test$style_collapsed)

csrf_acc

Final Thoughts

Style first, forgiveness later?

  • One reason seems that beers are generally brewed with style in mind first (“let’s make a pale ale”) rather than deciding the beer’s style after determining its characteristics and idiosyncrasies
    • Even if the beer turns out more like a sour, and in a blind taste test might be classified as a sour more often than a pale ale, it still gets the label pale ale
    • This makes the style definitions broader and harder to predict

Future Directions

  • Hierarchical clustering
  • Incorporate flavor profiles for beers sourced/scraped from somewhere
  • Implement a GAN to come up with beer names
  • More on the hops deep dive: which hops are used most often in which styles?

sessionInfo()
LS0tCnRpdGxlOiBEYXRhIFNjaWVuY2UgTXVzaW5ncyBvbiBCZWVyCmF1dGhvcjoKICBuYW1lOiBBbWFuZGEgRG9iYnluCmRhdGU6ICdgciBmb3JtYXQoU3lzLnRpbWUoKSwgIiVCICVkLCAlWSIpYCcKIyBvdXRwdXQ6CiMgICBodG1sX25vdGVib29rOgojICAgICB0b2M6IGZhbHNlCiMgICAgIHRoZW1lOiB5ZXRpCiMgICBwZGZfZG9jdW1lbnQ6CiMgICAgIGtlZXBfdGV4OiB0cnVlCiMgICAgIHRvYzogZmFsc2UKIyAgIGdpdGh1Yl9kb2N1bWVudDoKIyAgICAgdG9jOiB0cnVlCiAgICAKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICBrZWVwX21kOiB0cnVlCiAgICB0b2M6IGZhbHNlCiAgICB0aGVtZTogeWV0aQogIGdpdGh1Yl9kb2N1bWVudDoKICAgIHRvYzogdHJ1ZQotLS0KCmBgYHtyLCBldmFsPUZBTFNFLCBlY2hvPUZBTFNFfQojIElmIG5lZWQgdG8gY2xvc2UgYWxsIGNvbm5lY3Rpb25zCmxhcHBseSggZGJMaXN0Q29ubmVjdGlvbnMoIGRiRHJpdmVyKCBkcnYgPSAiTXlTUUwiKSksIGRiRGlzY29ubmVjdCkKYGBgCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0KbGlicmFyeShrbml0cikKbGlicmFyeShicm9vbSkKCiMga25pdHI6Om9wdHNfa25pdCRzZXQocm9vdC5kaXI9bm9ybWFsaXplUGF0aCgnLi4vJykpCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gRkFMU0UsIHdhcm5pbmcgPSBGQUxTRSkKa25pdHI6Om9wdHNfY2h1bmskc2V0KGZpZy53aWR0aD0xMiwgZmlnLmhlaWdodD04KSAKb3B0aW9ucyhrbml0ci50YWJsZS5mb3JtYXQgPSAnbWFya2Rvd24nKQpgYGAKCgojIyMgTW90aXZhdGlvbiBhbmQgT3ZlcnZpZXcKClRoaXMgaXMgYSBmaXJzdCBwYXNzIGV4cGxvcmF0aW9uIG9mIGRpZmZlcmVudCBhc3BlY3RzIG9mIGJlZXIuIFRoZSBkYXRhIHdhcyBjb2xsZWN0ZWQgdmlhIHRoZSBbQnJld2VyeURCXShodHRwOi8vd3d3LmJyZXdlcnlkYi5jb20vZGV2ZWxvcGVycykgQVBJLiBTcGVjaWFsIHRoYW5rcyB0byBbS3JpcyBLcm9za2ldKGh0dHBzOi8va3JvLnNraS8pIGZvciBkYXRhIGlkZWF0aW9uIGFuZCBjby1tZW1iZXJzaGlwIGluIHRoZSBob25vdXJhYmxlIHdvcmtwbGFjZSBiZWVyIGNvbnNvcnRpdW0uCgpUaGUgbWFpbiBxdWVzdGlvbiB0aGlzIGFuYWx5c2lzIGlzIG1lYW50IHRvIHRhY2tsZSBpczogQXJlIGJlZXIgc3R5bGVzIGFjdHVhbGx5IGluZGljYXRpdmUgb2Ygc2hhcmVkIGF0dHJpYnV0ZXMgb2YgdGhlIGJlZXJzIHdpdGhpbiB0aGF0IHN0eWxlPyBPciBhcmUgc3R5bGUgYm91bmRhcmllcyBtb3JlIG9yIGxlc3MgYXJiaXRyYXJ5PyBJIHRvb2sgdHdvIGFwcHJvYWNoZXMgdG8gdGhpczogdW5zdXBlcnZpc2VkIGNsdXN0ZXJpbmcgYW5kIHN1cGVydmlzZWQgcHJlZGljdGlvbi4gCgpDbHVzdGVycyBkZWZpbmVkIGJ5IHRoZSBhbGdvcml0aG0gd2VyZSBjb21wYXJlZCB0byB0aGUgc3R5bGUgImNlbnRlcnMiIGFzIGRlZmluZWQgYnkgdGhlIG1lYW4gQUJWLCBJQlUsIGFuZCBTUk0uIE9uIHRoZSBwcmVkaWN0aW9uIHNpZGUsIHByZWRpY3RvciB2YXJpYWJsZXMgZm9yIGluY2x1ZGUgQUJWIChhbGNvaG9sIGJ5IHZvbHVtZSksIElCVSAoaW50ZXJuYXRpb25hbCBiaXR0ZXJuZXNzIHVuaXRzKSwgU1JNIChbYSBtZWFzdXJlIG9mIGNvbG9yXShodHRwOi8vd3d3LnR3b2JlZXJkdWRlcy5jb20vYmVlci9zcm0pKSBhcyB3ZWxsIGFzIGluZ3JlZGllbnRzIGxpa2UgaG9wcyBhbmQgbWFsdHMuIFRoZSBvdXRjb21lIHZhcmlhYmxlIGlzIHRoZSBzdHlsZSB0aGF0IGJlZXIgd2FzIGFzc2lnbmVkLgoKVGhpcyBkb2N1bWVudCBzdGFydHMgb2ZmIHdpdGggYW4gZXhwbGFuYXRpb24gb2YgaG93IEkgc291cmNlZCBiZWVyIGRhdGEgZnJvbSBCcmV3ZXJ5REIsIGNsZWFuZWQgdGhhdCBkYXRhLCBhbmQgc3R1Y2sgdGhlIHBhcnRzIG9mIGl0IEkgd2FudGVkIGluIGEgZGF0YWJhc2UuIChUaGVzZSBhcmUganVzdCB0aGUgaGlnaGxpZ2h0czsgdGhlIGNvZGUgYWN0dWFsbHkgZXhlY3V0ZWQgaW4gdGhpcyBkb2N1bWVudCBxdWVyaWVzIHRoYXQgZGF0YWJhc2UsIHNwZWNpZmljYWxseSBieSBzb3VyY2luZyB0aGUgZmlsZSBgcmVhZF9mcm9tX2RiLlJgLCBhbHNvIGluIHRoaXMgcmVwbywgcmF0aGVyIHRoYW4gaGl0dGluZyB0aGUgQnJld2VyeURCIEFQSS4gVGhpcyBpcyBkb25lIGZvciBleHBlZGllbmN5J3Mgc2FrZSBhcyB0aGUgY29kZSBiZWxvdyBkZXRhaWxpbmcgaG93IHRvIGFjdHVhbGx5IGdldCB0aGUgYmVlciBkYXRhLCBydW4gaW4gZnVsbCBpbiBgcnVuX2l0LlJgLCB0YWtlcyBzb21lIHRpbWUgdG8gZXhlY3V0ZS4pCgpJdCB0aGVuIG1vdmVzIGludG8gY2x1c3RlcmluZyAoay1tZWFucykgYW5kIHByZWRpY3Rpb24gKG5ldXJhbCBuZXQsIHJhbmRvbSBmb3Jlc3QpLiBCZWxvdyBpcyBhIG1vcmUgZGV0YWlsZWQgb3ZlcnZpZXcgb2YgdGhlIGdlbmVyYWwgd29ya2Zsb3cuCgoKIyMjIFdvcmtmbG93CgoqKjEuIEdldCBhbmQgUHJlcGFyZSoqCgpXaGVuIHdlIGZpcnN0IGhpdCB0aGUgQnJld2VyeURCIEFQSSB0byBpdGVyYXRpdmVseSBwdWxsIGluIGFsbCBiZWVycyBhbmQgdGhlaXIgaW5ncmVkaWVudHMgYWxvbmcgd2l0aCBvdGhlciB0aGluZ3Mgd2UgbWlnaHQgd2FudCBsaWtlIGJyZXdlcmllcyBhbmQgZ2xhc3N3YXJlLiBUaGVuIHdlIHVubmVzdCB0aGUgSlNPTiByZXNwb25zZXMsIGluY2x1ZGluZyBhbGwgdGhlIGluZ3JlZGllbnRzIGNvbHVtbnMsIGFuZCBkdW1wIHRoaXMgYWxsIGludG8gYSBNeVNRTCBkYXRhYmFzZS4KCk5leHQsIHdlIGNyZWF0ZSBhIGBzdHlsZV9jb2xsYXBzZWRgIGNvbHVtbiB0byByZWR1Y2UgdGhlIG51bWJlciBvZiBsZXZlbHMgb2Ygb3VyIG91dGNvbWUgdmFyaWFibGUuIFdlIGRvIHRoaXMgYnkgYGdyZXBgaW5nIHRocm91Z2ggZWFjaCBiZWVyJ3Mgc3R5bGUgdG8gZGV0ZXJtaW5lIGlmIHRoYXQgc3R5bGUgY29udGFpbnMgYSBrZXl3b3JkIHRoYXQgcXVhbGlmaWVzIGl0IHRvIGJlIHJvbGxlZCBpbnRvIGEgY29sbGFwc2VkIHN0eWxlOyBpZiBpdCBkb2VzLCBpdCBnZXRzIHRoYXQga2V5d29yZCBpbiBhIGBzdHlsZV9jb2xsYXBzZWRgIGNvbHVtbi4KCkZpbmFsbHkgd2UgdW5uZXN0IHRoZSBpbmdyZWRpZW50cyBgaG9wc2AgYW5kIGBtYWx0c2AgaW50byBhIHdpZGUsIHNwYXJzZSBkYXRhZnJhbWUuIEluZGl2aWR1YWwgaW5ncmVkaWVudHMgYXJlIG5vdyBjb2x1bW5zLCB3aXRoIGVhY2ggYmVlciBzdGlsbCBpbiBpdHMgb3duIHJvd3M7IGEgY2VsbCBnZXRzIGEgMSBpZiBpbmdyZWRpZW50IGlzIHByZXNlbnQgYW5kIDAgb3RoZXJ3aXNlLiBUaGlzIGFsbG93cyBtb3JlIGdyYW51YWwgaW5mZXJlbmNlIGludG8gaW5ncmVkaWVudHMnIGVmZmVjdHMgb24gYm90aCBzdHlsZSBhbmQgYml0dGVybmVzcyAob2NjYXNpb25pbmcgYSBzaG9ydCBmb3JheSBpbnRvIGhvcHMpLgoKKioyLiBTaG9ydCBmb3JheSBpbnRvIGhvcHMqKgoKQSBxdWljayBsb29rIGF0IHRoZSBtb3N0IHBvcHVsYXIgaG9wcyBhbmQgYW4gZXhwbG9yYXRpb24gb2YgdGhlIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIGhvcHMgYW5kIGJpdHRlcm5lc3MuCgoKKiozLiBJbmZlcioqCgpDbHVzdGVyOiB1bnN1cGVydmlzZWQgay1tZWFucyBjbHVzdGVyaW5nIHBhcnRpdGlvbmluZyB0aGUgZW50aXJlIGRhdGFzZXQgaW50byB0ZW4gY2x1c3RlcnMuIE5leHQsIHdlIGNsdXN0ZXIgb24gYSBkYXRhc2V0IGNvbXBvc2VkIG9mIGp1c3QgZml2ZSBzZWxlY3RlZCBzdHlsZXMgaW50byBmaXZlIGNsdXN0ZXJzLiAKCldlIHRoZW4gYXR0ZW1wdCB0byBwcmVkaWN0IHByZWRpY3QgZWl0aGVyIGBzdHlsZWAgb3IgYHN0eWxlX2NvbGxhcHNlZGAgdXNpbmcgYSBuZXVyYWwgbmV0IGFuZCBhIHJhbmRvbSBmb3Jlc3QuIFRoZSBtYWluIHByZWRpY3RvcnMgYXJlIEFCViwgSUJVLCBTUk0sIHRvdGFsIG51bWJlciBvZiBob3BzLCBhbmQgdG90YWwgbnVtYmVyIG9mIG1hbHRzLiBUaGUgZ2xhc3MgYSBiZWVyIGlzIHNlcnZlZCBpbiBpcyBhbHNvIGNvbnNpZGVyZWQuIAoKCiMjIyBUaG91Z2h0cwoKVGhlIHF1ZXN0aW9uIG9mIHdoYXQgc2hvdWxkIGJlIGEgcHJlZGljdG9yIHZhcmlhYmxlIGZvciBzdHlsZSBpcyBhIGJpdCBtdXJreSBoZXJlLiBXaGF0IHNob3VsZCBiZSBmYWlyIGdhbWUgZm9yIHByZWRpY3Rpbmcgc3R5bGUgYW5kIHdoYXQgc2hvdWxkbid0PyBDaGFyYWN0ZXJpc3RpY3Mgb2YgYSBiZWVyIHRoYXQgYXJlIGRlZmluZWQgKmJ5KiBpdHMgc3R5bGUgd291bGQgc2VlbSB0byBiZSAiY2hlYXRpbmciIGluIGEgd2F5LiBUaGUgb25seSAiaW5wdXRzIiB0byBhIGJlZXIgd2UgaGF2ZSBpbiBvdXIgZGF0YXNldCBhcmUgaXRzIGluZ3JlZGllbnRzLCBwcmltYXJseSBob3BzIGFuZCBtYWx0cy4gV2hpbGUgdGhlc2UgY2VydGFpbmx5IGhhdmUgYW4gZWZmZWN0IG9uIGl0cyBmbGF2b3IgcHJvZmlsZSwgSSBjb25zaWRlciB0aGVtIHNlbWktY2hlYXRpbmcgYmVjYXVzZSBpZiBzdHlsZSBpcyBkZXRlcm1pbmVkIGJlZm9yZWhhbmQgaXQgbGlrZWx5IGRldGVybWluZXMgYXQgbGVhc3QgaW4gcGFydCB3aGljaCBpbmdyZWRpZW50cyBhcmUgYWRkZWQuIFRoZSBtYWluIGNhbmRpZGF0ZXMgaW4gbXkgbWluZCBhcmUgQUJWLCBJQlUsIGFuZCBTUk0uIFRoZXNlIGFyZSAib3V0cHV0cyIgb2YgYSBiZWVyIChpbiB0aGUgc2Vuc2UgdGhhdCB0aGV5IGNhbiBvbmx5IGJlIGV4YWN0bHkgZGV0ZXJtaW5lZCBvbmNlIGEgYmVlciBpcyBicmV3ZXJlZCkgdGhhdCBtZWFuaW5nZnVsbHkgZGVmaW5lIGl0LiBXaGlsZSBjb3JyZWxhdGVkIEFCViBpcyBjb3JyZWxhdGVkIHdpdGggYm90aCBJQlUgYW5kIFNSTSwgdGhlIHRocmVlIGFyZSB0aGVvcmV0aWNhbGx5IG9ydGhvZ29uYWwgdG8gZWFjaCBvdGhlci4gQSBzdHlsZS1kZWZpbmVkIGF0dHJpYnV0ZSBsaWtlIGdsYXNzIHR5cGUgaXMgYSBiYWQgY2FuZGlkYXRlIGZvciBhIHByZWRpY3RvciB2YXJpYWJsZSBiZWNhdXNlIGl0IGlzIGNvbXBsZXRlbHkgZGVjb3VwbGVkIGZyb20gdGhlIGJlZXIgaXRzZWxmIGFuZCBkZXRlcm1pbmVkIGVudGlyZWx5IGJ5IHRoZSBzdHlsZSB0aGUgYmVlciBoYXMgYmVlbiBhc3NpZ25lZCB0by4KClRoZSBhbnN3ZXIgdGh1cyBmYXIgc2VlbXMgdG8gYmUgdGhhdCB0aGUgYmVlciBsYW5kc2NhcGUgaXMgbW9yZSBvZiBhIHNwZWN0cnVtIHRoYW4gYSBjb2xsZWN0aW9uIG9mIG5lYXRseSBkaWZmZXJlbnRpYXRlZCBzdHlsZXMuIEJlZXItaW50cmluc2ljIGF0dHJpYnV0ZXMgbGlrZSBiaXR0ZXJuZXNzIGFyZW4ndCBncmVhdCBwcmVkaWN0b3JzIG9mIHN0eWxlLiBUaGUgcmVsYXRpdmUgaW1wb3J0YW5jZSBvZiBkaWZmZXJlbnQgdmFyaWFibGVzIGRlcGVlbmRzIG9uIHRoZSBwcmVkaWN0aW9uIG1ldGhvZCB1c2VkLiBIb3dldmVyLCBvbmUgc3R5bGUtZGVmaW5lZCBhdHRyaWJ1dGUsIHRoZSBnbGFzcyBhIGJlZXIgaXMgc2VydmVkIGluLCBpbmNyZWFzZWQgdGhlIGFjY3VyYWN5IG9mIHByZWRpY3Rpb24gc3Vic3RhbnRpYWxseS4KCk9mIGNvdXJzZSwgb3RoZXIgaW1wb3J0YW50IGFzcGVjdHMgb2YgdGhlIGZsYXZvciwgYm9keSwgc21lbGwsIGV0Yy4gb2YgdGhlIGJlZXJzIGNvdWxkIG5vdCBiZSBjb25zaWRlcmVkIGJlY2F1c2UgdGhpcyBkYXRhIGlzIG5vdCBhdmFpbGFibGUgZnJvbSBCcmV3ZXJ5REIuCgoKIVtdKC4vdGFwcy5qcGcpCgoKCioqKgoKYGBge3IsIGVjaG89VFJVRSwgbWVzc2FnZT1GQUxTRX0Kc291cmNlKCIuL3JlYWRfZnJvbV9kYi5SIikKYGBgCgoKIyMgR2V0IGFuZCBQcmVwYXJlIERhdGEKCioqR0VUdGluZyBiZWVyLCB0aGUgYWdlLW9sZCBkaWxlbW1hKioKCiogVGhlIEJyZXdlcnlEQiBBUEkgcmV0dXJucyBhIGNlcnRhaW4gbnVtYmVyIG9mIHJlc3VsdHMgcGVyIHBhZ2U7IGlmIHdlIHdhbnQgCiogU28sIHdlIGhpdCB0aGUgQnJld2VyeURCIEFQSSBhbmQgYXNrIGZvciBgMTpudW1iZXJfb2ZfcGFnZXNgCiAgICAqIFdlIGNhbiBjaGFuZ2UgYG51bWJlcl9vZl9wYWdlc2AgdG8sIGUuZy4sIDMgaWYgd2Ugb25seSB3YW50IHRoZSBmaXJzdCAzIHBhZ2VzCiAgICAqIElmIHRoZXJlJ3Mgb25seSBvbmUgcGFnZSAoYXMgaXMgdGhlIGNhc2UgZm9yIHRoZSBnbGFzc3dhcmUgZW5kcG9pbnQpLCBudW1iZXJPZlBhZ2VzIHdvbid0IGJlIHJldHVybmVkLCBzbyBpbiB0aGlzIGNhc2Ugd2Ugc2V0IG51bWJlcl9vZl9wYWdlcyB0byAxCiogVGhlIGBhZGRpdGlvbmAgcGFyYW1ldGVyIGNhbiBiZSBhbiBlbXB0eSBzdHJpbmcgaWYgbm90aGluZyBlbHNlIGlzIG5lZWRlZAoKYGBge3IsIGV2YWwgPSBGQUxTRSwgZWNobz1UUlVFfQoKYmFzZV91cmwgPC0gImh0dHA6Ly9hcGkuYnJld2VyeWRiLmNvbS92MiIKa2V5X3ByZWZhY2UgPC0gIi8/a2V5PSIKCnBhZ2luYXRlZF9yZXF1ZXN0IDwtIGZ1bmN0aW9uKGVwLCBhZGRpdGlvbiwgdHJhY2VfcHJvZ3Jlc3MgPSBUUlVFKSB7ICAgIAogIGZ1bGxfcmVxdWVzdCA8LSBOVUxMCiAgZmlyc3RfcGFnZSA8LSBmcm9tSlNPTihwYXN0ZTAoYmFzZV91cmwsICIvIiwgZXAsICIvIiwga2V5X3ByZWZhY2UsIGtleQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICwgIiZwPTEiKSkKICBudW1iZXJfb2ZfcGFnZXMgPC0gaWZlbHNlKCEoaXMubnVsbChmaXJzdF9wYWdlJG51bWJlck9mUGFnZXMpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBmaXJzdF9wYWdlJG51bWJlck9mUGFnZXMsIDEpICAgICAgCgogICAgZm9yIChwYWdlIGluIDE6bnVtYmVyX29mX3BhZ2VzKSB7ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgdGhpc19yZXF1ZXN0IDwtIGZyb21KU09OKHBhc3RlMChiYXNlX3VybCwgIi8iLCBlcCwgIi8iLCBrZXlfcHJlZmFjZSwga2V5CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICwgIiZwPSIsIHBhZ2UsIGFkZGl0aW9uKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmbGF0dGVuID0gVFJVRSkgCiAgICB0aGlzX3JlcV91bm5lc3RlZCA8LSB1bm5lc3RfaXQodGhpc19yZXF1ZXN0KSAgICAjICA8LSByZXF1ZXN0IHVubmVzdGVkIGhlcmUKICAgIGlmKHRyYWNlX3Byb2dyZXNzID09IFRSVUUpIHttZXNzYWdlKHBhc3RlMCgiUGFnZSAiLCB0aGlzX3JlcV91bm5lc3RlZCRjdXJyZW50UGFnZSkpfQogICAgZnVsbF9yZXF1ZXN0IDwtIGJpbmRfcm93cyhmdWxsX3JlcXVlc3QsIHRoaXNfcmVxX3VubmVzdGVkW1siZGF0YSJdXSkKICB9CiAgcmV0dXJuKGZ1bGxfcmVxdWVzdCkKfSAKCmFsbF9iZWVyX3JhdyA8LSBwYWdpbmF0ZWRfcmVxdWVzdCgiYmVlcnMiLCAiJndpdGhJbmdyZWRpZW50cz1ZIikKYGBgCgoKCiogRnVuY3Rpb24gZm9yIHVubmVzdGluZyBKU09OIHVzZWQgaW5zaWRlIGBwYWdpbmF0ZWRfcmVxdWVzdCgpYCBiZWxvdwogICAgKyBUYWtlcyB0aGUgY29sdW1uIG5hbWVkIGBuYW1lYCBuZXN0ZWQgd2l0aGluIGEgY29sdW1uIGluIHRoZSBkYXRhIHBvcnRpb24gb2YgdGhlIHJlc3BvbnNlCiAgICAgICAgKyBJZiB0aGUgYG5hbWVgIGNvbHVtbiBkb2Vzbid0IGV4aXN0LCBpdCB0YWtlcyB0aGUgZmlyc3QgbmVzdGVkIGNvbHVtbgoqIFdlIHVzZSBzb21ldGhpbmcgc2ltaWxhciB0byB1bm5lc3QgaW5ncmVkaWVudCBsaWtlIGFsbCBvZiBhIGJlZXIncyBob3BzIGFuZCBtYWx0cyBpbnRvIGEgbG9uZyBzdHJpbmcgY29udGFpbmVkIGluIGBob3BzX25hbWVgIGFuZCBgbWFsdF9uYW1lYAoKYGBge3IsIGV2YWw9RkFMU0UsIGVjaG89VFJVRX0KdW5uZXN0X2l0IDwtIGZ1bmN0aW9uKGRmKSB7CiAgdW5uZXN0ZWQgPC0gZGYKICBmb3IoY29sIGluIHNlcV9hbG9uZyhkZltbImRhdGEiXV0pKSB7CiAgICBpZighIGlzLm51bGwobmNvbChkZltbImRhdGEiXV1bW2NvbF1dKSkpIHsKICAgICAgaWYoISBpcy5udWxsKGRmW1siZGF0YSJdXVtbY29sXV1bWyJuYW1lIl1dKSkgewogICAgICAgIHVubmVzdGVkW1siZGF0YSJdXVtbY29sXV0gPC0gZGZbWyJkYXRhIl1dW1tjb2xdXVtbIm5hbWUiXV0KICAgICAgfSBlbHNlIHsKICAgICAgICB1bm5lc3RlZFtbImRhdGEiXV1bW2NvbF1dIDwtIGRmW1siZGF0YSJdXVtbY29sXV1bWzFdXQogICAgICB9CiAgICB9CiAgfQogIHJldHVybih1bm5lc3RlZCkKfQpgYGAKCgoKKipDb2xsYXBzZSBTdHlsZXMqKgoKKiBTYXZlIHRoZSBtb3N0IHBvcHVsYXIgc3R5bGVzIGluIGBrZXl3b3Jkc2AKKiBMb29wIHRocm91Z2ggZWFjaCBrZXl3b3JkCiAgICAqIEZvciBlYWNoIGJlZXIsIGBncmVwYCB0aHJvdWdoIGl0cyBzdHlsZSBjb2x1bW4gdG8gc2VlIGlmIGl0IGNvbnRhaW5zIGFueSBvbmUgb2YgdGhlc2Uga2V5d29yZHMKICAgICogSWYgaXQgZG9lcywgZ2l2ZSBpdCB0aGF0IGtleXdvcmQgaW4gYSBuZXcgY29sdW1uIGBzdHlsZV9jb2xsYXBzZWRgCiogSWYgYSBiZWVyJ3MgbmFtZSBtYXRjaGVzIG11bHRpcGxlIGtleXdvcmRzLCBlLmcuLCBBbWVyaWNhbiBEb3VibGUgSW5kaWEgUGFsZSBBbGUgd291bGQgbWF0Y2ggRG91YmxlIEluZGlhIFBhbGUgQWxlLCBJbmRpYSBQYWxlIEFsZSwgYW5kIFBhbGUgQWxlLCBpdHMgYHN0eWxlX2NvbGxhcHNlZGAgaXMgdGhlICoqbGFzdCoqIG9mIHRob3NlIHRoYXQgYXBwZWFyIGluIGtleXdvcmRzIAogICAgKiBUaGlzIGlzIHdoeSBrZXl3b3JkcyBhcmUgaW50ZW50aW9uYWxseSBvcmRlcmVkIGZyb20gbW9zdCBnZW5lcmFsIHRvIG1vc3Qgc3BlY2lmaWMKICAgICogU28gaW4gdGhlIGNhc2Ugb2YgYW4gY2FzZSBvZiBBbWVyaWNhbiBEb3VibGUgSW5kaWEgUGFsZSBBbGU6IHNpbmNlIERvdWJsZSBJbmRpYSBQYWxlIEFsZSBhcHBlYXJzIGluIGBrZXl3b3Jkc2AgYWZ0ZXIgSW5kaWEgUGFsZSBBbGUgYW5kIFBhbGUgQWxlLCBhbiBBbWVyaWNhbiBEb3VibGUgSW5kaWEgUGFsZSBBbGUgd291bGQgZ2V0IGEgYHN0eWxlX2NvbGxhcHNlZGAgb2YgRG91YmxlIEluZGlhIFBhbGUgQWxlCiogSWYgbm8ga2V5d29yZCBpcyBjb250YWluZWQgaW4gYHN0eWxlYCwgYHN0eWxlX2NvbGxhcHNlZGAgaXMganVzdCB3aGF0ZXZlcidzIGluIGBzdHlsZWA7IGluIG90aGVyIHdvcmRzLCBpdCBkb2Vzbid0IGdldCBjb2xscHNlZCBpbnRvIGEgYmlnZ2VyIGJ1Y2tldAogICAgKiBUaGlzIGlzbid0IGEgaHVnZSBwcm9ibGVtIGJlY2F1c2Ugd2UnbGwgcGFyZSBkb3duIHRvIGp1c3QgdGhlIG1vc3QgcG9wdWxhciBzdHlsZXMgbGF0ZXIsIGhvd2V2ZXIgd2UgY291bGQgdGhpbmsgYWJvdXQgY3JlYXRpbmcgYSBjYXRjaGFsbCAiT3RoZXIiIGxldmVsIGZvciBgc3R5bGVfY29sbGFwc2VkYAogIApBIGxpc3Qgb2Ygb3VyIG1haW4gc3R5bGVzIHRvIGNvbGxhcHNlIHRvOgpgYGB7ciwgZWNobz1UUlVFfQprZXl3b3JkcyA8LSBjKCJMYWdlciIsICJQYWxlIEFsZSIsICJJbmRpYSBQYWxlIEFsZSIsICJEb3VibGUgSW5kaWEgUGFsZSBBbGUiLCAiSW5kaWEgUGFsZSBMYWdlciIsICJIZWZld2VpemVuIiwgIkJhcnJlbC1BZ2VkIiwiV2hlYXQiLCAiUGlsc25lciIsICJQaWxzZW5lciIsICJBbWJlciIsICJHb2xkZW4iLCAiQmxvbmRlIiwgIkJyb3duIiwgIkJsYWNrIiwgIlN0b3V0IiwgIlBvcnRlciIsICJSZWQiLCAiU291ciIsICJLw7Zsc2NoIiwgIlRyaXBlbCIsICJCaXR0ZXIiLCAiU2Fpc29uIiwgIlN0cm9uZyBBbGUiLCAiQmFybGV5IFdpbmUiLCAiRHViYmVsIiwgIkFsdGJpZXIiKQoKa2V5d29yZF9kZiA8LSBhc190aWJibGUobGlzdChgTWFpbiBTdHlsZXNgID0ga2V5d29yZHMpKQprYWJsZShrZXl3b3JkX2RmKQpgYGAKCgpgYGB7ciwgZXZhbD1GQUxTRSwgZWNobz1UUlVFfQoKY29sbGFwc2Vfc3R5bGVzIDwtIGZ1bmN0aW9uKGRmLCB0cmFjZV9wcm9ncmVzcyA9IFRSVUUpIHsKICAKICBkZltbInN0eWxlX2NvbGxhcHNlZCJdXSA8LSB2ZWN0b3IobGVuZ3RoID0gbnJvdyhkZikpCiAgCiAgZm9yIChiZWVyIGluIDE6bnJvdyhkZikpIHsKICAgIGlmIChncmVwbChwYXN0ZShrZXl3b3JkcywgY29sbGFwc2U9InwiKSwgZGYkc3R5bGVbYmVlcl0pKSB7ICAgIAogICAgICBmb3IgKGtleXdvcmQgaW4ga2V5d29yZHMpIHsgICAgICAgICAKICAgICAgICBpZihncmVwbChrZXl3b3JkLCBkZiRzdHlsZVtiZWVyXSkgPT0gVFJVRSkgewogICAgICAgICAgZGYkc3R5bGVfY29sbGFwc2VkW2JlZXJdIDwtIGtleXdvcmQgICAgCiAgICAgICAgfSAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgfSAKICAgIH0gZWxzZSB7CiAgICAgIGRmJHN0eWxlX2NvbGxhcHNlZFtiZWVyXSA8LSBhcy5jaGFyYWN0ZXIoZGYkc3R5bGVbYmVlcl0pICAgICAgIAogICAgfQogICAgaWYodHJhY2VfcHJvZ3Jlc3MgPT0gVFJVRSkge21lc3NhZ2UocGFzdGUwKCJDb2xsYXBzaW5nIHRoaXMgIiwgZGYkc3R5bGVbYmVlcl0sICIgdG86ICIsIGRmJHN0eWxlX2NvbGxhcHNlZFtiZWVyXSkpfQogIH0KICByZXR1cm4oZGYpCn0KCmBgYAoKKiBUaGVuIHdlIGNvbGxhcHNlIGZ1cnRoZXI7IHJpZ2h0IG5vdyB3ZSBqdXN0IGNvbWJpbmUgYWxsIHdoZWF0eSBiZWFycyBpbnRvIFdoZWF0IGFuZCBQaWxzLWxpa2UgYmVlcnMgaW50byBQaWxzZW5lciAod2l0aCB0d28gZSdzKSBieSBgZmN0X2NvbGxhcHNlYGluZyB0aG9zZSBsZXZlbHMKCmBgYHtyLCBlY2hvPVRSVUUsIGV2YWw9RkFMU0V9CmNvbGxhcHNlX2Z1cnRoZXIgPC0gZnVuY3Rpb24oZGYpIHsKICBkZltbInN0eWxlX2NvbGxhcHNlZCJdXSA8LSBkZltbInN0eWxlX2NvbGxhcHNlZCJdXSAlPiUKICAgIGZjdF9jb2xsYXBzZSgKICAgICAgIldoZWF0IiA9IGMoIkhlZmV3ZWl6ZW4iLCAiV2hlYXQiKSwKICAgICAgIlBpbHNlbmVyIiA9IGMoIlBpbHNuZXIiLCAiQW1lcmljYW4tU3R5bGUgUGlsc2VuZXIiKSAjIHBpbHNlbmVyID09IHBpbHNuZXIgPT0gcGlscwogICAgKQogIHJldHVybihkZikKfQpgYGAKCgoKKipTcGxpdCBvdXQgSW5ncmVkaWVudHMqKgoKV2hlbiB3ZSB1bm5lc3RlZCBpbmdyZWRpZW50cywgd2UganVzdCBjb25jYXRlbmF0ZWQgYWxsIG9mIHRoZSBpbmdyZWRpZW50cyBmb3IgYSBnaXZlbiBiZWVyIGludG8gYSBsb25nIHN0cmluZy4gSWYgd2Ugd2FudCwgd2UgY2FuIHNwbGl0IG91dCB0aGUgaW5ncmVkaWVudHMgdGhhdCB3ZXJlIGNvbmNhdGVuYXRlZCBpbiBgPGluZ3JlZGllbnQ+X25hbWVgIHdpdGggdGhpcyBgc3BsaXRfaW5ncmVkaWVudHNgIGZ1bmN0aW9uLgoKVGhpcyB0YWtlcyBhIHZlY3RvciBvZiBgaW5ncmVkaWVudHNfdG9fc3BsaXRgLCBzbyBlLmcuIGBjKCJob3BzX25hbWUiLCAibWFsdF9uYW1lIilgIGFuZCBjcmVhdGVzIG9uZSBjb2x1bW4gZm9yIGVhY2ggdHlwZSBvZiBpbmdyZWRpZW50IChgaG9wc19uYW1lXzFgLCBgaG9wc19uYW1lXzJgLCBldGMuKS4gSXQncyBmbGV4aWJsZSBlbm91Z2ggdG8gYWRhcHQgaWYgZGF0YSBpbiBCcmV3ZXJ5REIgY2hhbmdlcyBhbmQgYSBiZWVyIG5vdyBoYXMgMTUgaG9wcyB3aGVyZSBiZWZvcmUgdGhlIG1heGltdW0gbnVtYmVyIG9mIGhvcHMgYSBiZWVyIGhhZCB3YXMgMTAuCgpgYGB7ciwgZXZhbD1GQUxTRSwgZWNobz1UUlVFfQpzcGxpdF9pbmdyZWRpZW50cyA8LSBmdW5jdGlvbihkZiwgaW5ncmVkaWVudHNfdG9fc3BsaXQpIHsKICAKICBuY29sX2RmIDwtIG5jb2woZGYpCiAgCiAgZm9yIChpbmdyZWRpZW50IGluIGluZ3JlZGllbnRzX3RvX3NwbGl0KSB7CgogICAgaW5ncmVkaWVudF9zcGxpdCA8LSBzdHJfc3BsaXQoZGZbW2luZ3JlZGllbnRdXSwgIiwgIikgICAgCiAgICBudW1fbmV3X2NvbHMgPC0gbWF4KGxlbmd0aHMoaW5ncmVkaWVudF9zcGxpdCkpICAgIAogIAogICAgZm9yIChudW0gaW4gMTpudW1fbmV3X2NvbHMpIHsKICAgICAgCiAgICAgIHRoaXNfY29sIDwtIG5jb2xfZGYgKyAxICAgICAgICAgCiAgICAgIAogICAgICBkZlssIHRoaXNfY29sXSA8LSBOQQogICAgICBuYW1lcyhkZilbdGhpc19jb2xdIDwtIHBhc3RlMChpbmdyZWRpZW50LCAiXyIsIG51bSkKICAgICAgbmNvbF9kZiA8LSBuY29sKGRmKSAgICAgICAgICAgICAKICAgICAgZm9yIChyb3cgaW4gc2VxX2Fsb25nKGluZ3JlZGllbnRfc3BsaXQpKSB7ICAgICAgICAgIAogICAgICAgIGlmICghaXMubnVsbChpbmdyZWRpZW50X3NwbGl0W1tyb3ddXVtudW1dKSkgeyAgICAgICAgCiAgICAgICAgICBkZltyb3csIHRoaXNfY29sXSA8LSBpbmdyZWRpZW50X3NwbGl0W1tyb3ddXVtudW1dCiAgICAgICAgfQogICAgICB9CiAgICAgIGRmW1tuYW1lcyhkZilbdGhpc19jb2xdXV0gPC0gZmFjdG9yKGRmW1tuYW1lcyhkZilbdGhpc19jb2xdXV0pCiAgICB9CiAgICAKICAgIG5jb2xfZGYgPC0gbmNvbChkZikKICB9CiAgcmV0dXJuKGRmKQp9CmBgYAoKClNvbWUgcXVpY2sgc3VtbWFyeSBzdGF0cyBvbiBvdXIgbWFpbiBkYXRhZnJhbWUgY2FsbGVkIGBiZWVyX25lY2Vzc2l0aWVzYDoKYGBge3IsIGVjaG89VFJVRX0KZGltKGJlZXJfbmVjZXNzaXRpZXMpCnN0cihiZWVyX25lY2Vzc2l0aWVzKQpgYGAKCgoqKkZpbmQgdGhlIE1vc3QgUG9wdWFsYXIgU3R5bGVzKioKCldlIGZpbmQgbWVhbiBBQlYsIElCVSwgYW5kIFNSTSBwZXIgY29sbGFwc2VkIHN0eWxlIGFuZCBhcnJhbmdlIGNvbGxhcHNlZCBzdHlsZXMgYnkgdGhlIG51bWJlciBvZiBiZWVycyB0aGF0IGZhbGwgaW50byB0aGVtLiAoVGhpcyBpcyBvZiBjb3Vyc2UgZGVwZW5kZW50IG9uIGhvdyB3ZSBjb2xsYXBzZSBzdHlsZXM7IGlmIHdlIGxvb3BlZCBhbGwgRG91YmxlIElQQXMgaW4gd2l0aCBJUEFzIHRoZW4gdGhlIGNhdGVnb3J5IElQQSB3b3VsZCBiZSBtdWNoIGJpZ2dlciB0aGFuIGl0IGlzIGlmIHdlIGtlZXAgdGhlIHR3byBzZXBhcmF0ZS4pCgpgYGB7ciwgZXZhbD1UUlVFLCBlY2hvPVRSVUV9CmxpYnJhcnkoZm9yY2F0cykKCiMgUGFyZSBkb3duIHRvIG9ubHkgY2FzZXMgd2hlcmUgc3R5bGUgaXMgbm90IE5BCmJlZXJfZGF0X3BhcmVkIDwtIGJlZXJfbmVjZXNzaXRpZXNbY29tcGxldGUuY2FzZXMoYmVlcl9uZWNlc3NpdGllcyRzdHlsZSksIF0KCiMgQXJyYW5nZSBiZWVyIGRhdCBieSBzdHlsZSBwb3B1bGFyaXR5CnN0eWxlX3BvcHVsYXJpdHkgPC0gYmVlcl9kYXRfcGFyZWQgJT4lIAogIGdyb3VwX2J5KHN0eWxlKSAlPiUgCiAgY291bnQoKSAlPiUgCiAgYXJyYW5nZShkZXNjKG4pKQoKIyBBZGQgYSBjb2x1bW4gdGhhdCBzY2FsZXMgcG9wdWxhcml0eQpzdHlsZV9wb3B1bGFyaXR5IDwtIGJpbmRfY29scyhzdHlsZV9wb3B1bGFyaXR5LCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fc2NhbGVkID0gYXMudmVjdG9yKHNjYWxlKHN0eWxlX3BvcHVsYXJpdHkkbikpKQoKIyBGaW5kIHN0eWxlcyB0aGF0IGFyZSBhYm92ZSBhIHotc2NvcmUgb2YgMApwb3B1bGFyX3N0eWxlcyA8LSBzdHlsZV9wb3B1bGFyaXR5ICU+JSAKICBmaWx0ZXIobl9zY2FsZWQgPiAwKQoKIyBQYXJlIGRhdCBkb3duIHRvIG9ubHkgYmVlcnMgdGhhdCBmYWxsIGludG8gdGhvc2Ugc3R5bGVzCnBvcHVsYXJfYmVlcl9kYXQgPC0gYmVlcl9kYXRfcGFyZWQgJT4lIAogIGZpbHRlcigKICAgIHN0eWxlICVpbiUgcG9wdWxhcl9zdHlsZXMkc3R5bGUKICApICU+JSAKICBkcm9wbGV2ZWxzKCkgJT4lIAogIGFzX3RpYmJsZSgpIApgYGAKCgpIb3cgbWFueSByb3dzIGRvIHdlIGhhdmUgaW4gb3VyIGRhdGFzZXQgb2YganVzdCBiZWVycyB0aGF0IGZhbGwgaW50byB0aGUgcG9wdWxhciBzdHlsZXM/CmBgYHtyLCBlY2hvPVRSVUV9Cm5yb3cocG9wdWxhcl9iZWVyX2RhdCkKYGBgCgoKTm93IHdlIGZpbmQgdGhlIHN0eWxlIGNlbnRlcnMuCmBgYHtyLCBlY2hvPVRSVUV9CiMgRmluZCB0aGUgY2VudGVycyAobWVhbiBhYnYsIGlidSwgc3JtKSBvZiB0aGUgbW9zdCBwb3B1bGFyIHN0eWxlcwpzdHlsZV9jZW50ZXJzIDwtIHBvcHVsYXJfYmVlcl9kYXQgJT4lIAogIGdyb3VwX2J5KHN0eWxlX2NvbGxhcHNlZCkgJT4lIAogIGFkZF9jb3VudCgpICU+JSAKICBzdW1tYXJpc2UoCiAgICBtZWFuX2FidiA9IG1lYW4oYWJ2LCBuYS5ybSA9IFRSVUUpLAogICAgbWVhbl9pYnUgPSBtZWFuKGlidSwgbmEucm0gPSBUUlVFKSwgCiAgICBtZWFuX3NybSA9IG1lYW4oc3JtLCBuYS5ybSA9IFRSVUUpLAogICAgbiA9IG1lZGlhbihuLCBuYS5ybSA9IFRSVUUpICAgICAgICAgICMgTWVkaWFuIGhlcmUgb25seSBmb3Igc3VtbWFyaXNlLiBTaG91bGQgYmUganVzdCB0aGUgc2FtZSBhcyBuCiAgKSAlPiUgCiAgYXJyYW5nZShkZXNjKG4pKSAlPiUgCiAgZHJvcF9uYSgpICU+JSAKICBkcm9wbGV2ZWxzKCkKCiMgR2l2ZSBzb21lIG5pY2VyIG5hbWVzCnN0eWxlX2NlbnRlcnNfcmVuYW1lIDwtIHN0eWxlX2NlbnRlcnMgJT4lIAogIHJlbmFtZSgKICAgIGBDb2xsYXBzZWQgU3R5bGVgID0gc3R5bGVfY29sbGFwc2VkLAogICAgYE1lYW4gQUJWYCA9IG1lYW5fYWJ2LAogICAgYE1lYW4gSUJVYCA9IG1lYW5faWJ1LAogICAgYE1lYW4gU1JNYCA9IG1lYW5fc3JtLAogICAgYE51bWVyIG9mIEJlZXJzYCA9IG4KICApCmBgYAoKClRha2UgYSBsb29rIGF0IHRoZSB0YWJsZSwgb3JkZXJlZCBieSBudW1iZXIgb2YgYmVlcnMgaW4gdGhhdCBzdHlsZSwgZGVzY2VuZGluZy4gICAgICAKCmBgYHtyfQprYWJsZShzdHlsZV9jZW50ZXJzX3JlbmFtZSkKYGBgCgoKCioqKgoKIyMjIEluZ3JlZGllbnRzCgpUaGUgbGlmZWN5Y2xlIG9mIGluZ3JlZGllbnRzIHRodXMgZmFyIGhhcyBiZWVuIHRvIHVubmVzdCB0aGVtIGZyb20gdGhlIHJhdyBKU09OIGludG8gYSBsb25nIHN0cmluZyBjb250YWluZWQgaW4gYGhvcHNfbmFtZWAgYW5kIGBtYWx0X25hbWVgLiBOZXh0IGVhY2ggaW5ncmVkaWVudCBpbiBlYWNoIG9mIHRob3NlIGNvbHVtbnMgd2FzIHNwbGl0IG91dCBpbnRvIGBob3BzX25hbWVfMWAsIGBob3BzX25hbWVfMmAsIGV0Yy4KClRvIGdldCBtb3JlIGdyYW51bGFyIHdpdGggaW5ncmVkaWVudHMsIHdlIGNhbiBmdXJ0aGVyIHNwbGl0IG91dCBlYWNoIGluZGl2aWR1YWwgaW5ncmVkaWVudCBpbnRvIGl0cyBvd24gY29sdW1uLiBJZiBhIGJlZXIgb3Igc3R5bGUgY29udGFpbnMgdGhhdCBpbmdyZWRpZW50LCBpdHMgcm93IGdldHMgYSAxIGluIHRoYXQgaW5ncmVkaWVudCBjb2x1bW4gYW5kIGEgMCBvdGhlcndpc2UuCgpGcm9tIHRoaXMsIHdlIGNhbiBmaW5kIHRoZSB0b3RhbCBudW1iZXIgb2YgaG9wcyBhbmQgbWFsdHMgcGVyIGdyb3VwZXIuCgoKCiogVGhpcyBmdW5jdGlvbiB0YWtlcyBhIGRhdGFmcmFtZSBhbmQgdHdvIG90aGVyIHBhcmFtZXRlcnMgc2V0IGF0IHRoZSBvdXRzZXQ6CiAgICAqIGBpbmdyZWRpZW50X3dhbnRgOiB0aGlzIGNhbiBiZSBgaG9wc2AsIGBtYWx0YCwgb3Igb3RoZXIgaW5ncmVkaWVudHMgbGlrZSBgeWVhc3RgIGlmIHdlIHB1bGwgdGhhdCBpbgogICAgKiBgZ3JvdXBlcmA6IGNhbiBiZSBhIHZlY3RvciBvZiBvbmUgb3IgbW9yZSB0aGluZ3MgdG8gZ3JvdXAgYnksIGxpa2UgYmVlciBgaWRgIG9yIGBzdHlsZWAKICAgICAgKiBDYXJlZnVsIHdpdGggdXNpbmcgYG5hbWVgIGFzIGEgZ3JvdXBlciBhcyBtdWx0aXBsZSBiZWVycyBoYXZlIHRoZSBzYW1lIG5hbWUKCmBgYHtyLCBldmFsPVRSVUUsIGVjaG89VFJVRX0KCnBpY2tfaW5ncmVkaWVudF9nZXRfYmVlciA8LSBmdW5jdGlvbiAoaW5ncmVkaWVudF93YW50LCBkZiwgZ3JvdXBlcikgewogIAogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gU2V0dXAgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICMKICAjIFdlJ3ZlIGFscmVhZHkgc3BsaXQgaW5ncmVkaWVudCBudW1iZXIgbmFtZXMgb3V0IGZyb20gdGhlIGNvbmNhdGVuYXRlZCBzdHJpbmcgaW50byBjb2x1bW5zIGxpa2UgYG1hbHRfbmFtZV8xYCwKICAjIGBtYWx0X25hbWVfMmAsIGV0Yy4gV2UgbmVlZCB0byBmaW5kIHRoZSByYW5nZSBvZiB0aGVzZSBjb2x1bW5zOyB0aGVyZSB3aWxsIGJlIGEgZGlmZmVyZW50IG51bWJlciBvZiBtYWx0CiAgIyBjb2x1bW5zIHRoYW4gaG9wcyBjb2x1bW5zLCBmb3IgaW5zdGFuY2UuIFRoZSBmaXJzdCBvbmUgd2lsbCBiZSBgPGluZ3JlZGllbnQ+X25hbWVfMWAgYW5kIGZyb20gdGhpcyB3ZSBjYW4gZmluZAogICMgdGhlIGluZGV4IG9mIHRoaXMgY29sdW1uIGluIG91ciBkYXRhZnJhbWUuIFdlIGdldCB0aGUgbmFtZSBvZiBsYXN0IG9uZSB3aXRoIHRoZSBgZ2V0X2xhc3RfaW5nX25hbWVfY29sKClgCiAgIyBmdW5jdGlvbi4gVGhlbiB3ZSBzYXZlIGEgdmVjdG9yIG9mIGFsbCB0aGUgaW5ncmVkaWVudCBjb2x1bW4gbmFtZXMgaW4gYGluZ3JlZGllbnRfY29sbmFtZXNgLiBJdCB3aWxsIHN0YXkKICAjIGNvbnN0YW50IGV2ZW4gaWYgdGhlIGluZGljZXMgY2hhbmdlIHdoZW4gd2Ugc2VsZWN0IG91dCBjZXJ0YWluIGNvbHVtbnMuIAogIAogICMgRmlyc3QgaW5ncmVkaWVudAogIGZpcnN0X2luZ3JlZGllbnRfbmFtZSA8LSBwYXN0ZShpbmdyZWRpZW50X3dhbnQsICJfbmFtZV8xIiwgc2VwPSIiKQogIGZpcnN0X2luZ3JlZGllbnRfaW5kZXggPC0gd2hpY2goY29sbmFtZXMoZGYpPT1maXJzdF9pbmdyZWRpZW50X25hbWUpCiAgCiAgIyBHZXQgdGhlIGxhc3QgaW5ncmVkaWVudAogIGdldF9sYXN0X2luZ19uYW1lX2NvbCA8LSBmdW5jdGlvbihkZikgewogICAgZm9yIChjb2wgaW4gbmFtZXMoZGYpKSB7CiAgICAgIGlmIChncmVwbChwYXN0ZShpbmdyZWRpZW50X3dhbnQsICJfbmFtZV8iLCBzZXAgPSAiIiksIGNvbCkgPT0gVFJVRSkgewogICAgICAgIG5hbWVfbGFzdF9pbmdfY29sIDwtIGNvbAogICAgICB9CiAgICB9CiAgICByZXR1cm4obmFtZV9sYXN0X2luZ19jb2wpCiAgfQogIAogICMgTGFzdCBpbmdyZWRpZW50CiAgbGFzdF9pbmdyZWRpZW50X25hbWUgPC0gZ2V0X2xhc3RfaW5nX25hbWVfY29sKGRmKQogIGxhc3RfaW5ncmVkaWVudF9pbmRleCA8LSB3aGljaChjb2xuYW1lcyhkZik9PWxhc3RfaW5ncmVkaWVudF9uYW1lKQogIAogICMgVmVjdG9yIG9mIGFsbCB0aGUgaW5ncmVkaWVudCBjb2x1bW4gbmFtZXMKICBpbmdyZWRpZW50X2NvbG5hbWVzIDwtIG5hbWVzKGRmKVtmaXJzdF9pbmdyZWRpZW50X2luZGV4Omxhc3RfaW5ncmVkaWVudF9pbmRleF0KICAKICAjIE5vbi1pbmdyZWRpZW50IGNvbHVtbiBuYW1lcyB3ZSB3YW50IHRvIGtlZXAKICB0b19rZWVwX2NvbF9uYW1lcyA8LSBjKCJpZCIsICJjbHVzdGVyX2Fzc2lnbm1lbnQiLCAibmFtZSIsICJhYnYiLCAiaWJ1IiwgInNybSIsICJzdHlsZSIsICJzdHlsZV9jb2xsYXBzZWQiKQogIAogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSMgCiAgCiAgIyBJbnNpZGUgYGdhdGhlcl9pbmdyZWRpZW50cygpYCB3ZSB0YWtlIG91dCBzdXBlcmZsb3VzIGNvbHVtbiBuYW1lcyB0aGF0IGFyZSBub3QgaW4gYHRvX2tlZXBfY29sX25hbWVzYCBvciBvbmUgCiAgIyBvZiB0aGUgaW5ncmVkaWVudCBjb2x1bW5zLCBmaW5kIHdoYXQgdGhlIG5ldyBpbmdyZWRpZW50IGNvbHVtbiBpbmRpY2VzIGFyZSwgc2luY2UgdGhleSdsbCBoYXZlIGNoYW5nZWQgYWZ0ZXIgCiAgIyB3ZSBwYXJlZCBkb3duIGFuZCB0aGVuIGdhdGhlciBhbGwgb2YgdGhlIGluZ3JlZGllbnQgY29sdW1ucyAoZS5nLiwgYGhvcHNfbmFtZV8xYCkgaW50byBvbmUgbG9uZyBjb2x1bW4sIAogICMgYGluZ19rZXlzYCBhbmQgYWxsIHRoZSBhY3R1YWwgaW5ncmVkaWVudCBuYW1lcyAoZS5nLiwgQ2FzY2FkZSkgaW50byBgaW5nX25hbWVzYC4KICAKICAjIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIEdhdGhlciBjb2x1bW5zIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAjCiAgZ2F0aGVyX2luZ3JlZGllbnRzIDwtIGZ1bmN0aW9uKGRmLCBjb2xzX3RvX2dhdGhlcikgewogICAgdG9fa2VlcF9pbmRpY2VzIDwtIHdoaWNoKGNvbG5hbWVzKGRmKSAlaW4lIHRvX2tlZXBfY29sX25hbWVzKQogICAgCiAgICBzZWxlY3RlZF9kZiA8LSBkZlssIGModG9fa2VlcF9pbmRpY2VzLCBmaXJzdF9pbmdyZWRpZW50X2luZGV4Omxhc3RfaW5ncmVkaWVudF9pbmRleCldCiAgICAKICAgIG5ld19pbmdfaW5kaWNlcyA8LSB3aGljaChjb2xuYW1lcyhzZWxlY3RlZF9kZikgJWluJSBjb2xzX3RvX2dhdGhlcikgICAgIyBpbmRpY2VzIHdpbGwgaGF2ZSBjaGFuZ2VkIHNpbmNlIHdlIHBhcmVkIGRvd24gCiAgICAKICAgIGRmX2dhdGhlcmVkIDwtIHNlbGVjdGVkX2RmICU+JQogICAgICBnYXRoZXJfKAogICAgICAgIGtleV9jb2wgPSAiaW5nX2tleXMiLAogICAgICAgIHZhbHVlX2NvbCA9ICJpbmdfbmFtZXMiLAogICAgICAgIGdhdGhlcl9jb2xzID0gY29sbmFtZXMoc2VsZWN0ZWRfZGYpW25ld19pbmdfaW5kaWNlc10KICAgICAgKSAlPiUKICAgICAgbXV0YXRlKAogICAgICAgIGNvdW50ID0gMQogICAgICApCiAgICByZXR1cm4oZGZfZ2F0aGVyZWQpCiAgfQogIGJlZXJfZ2F0aGVyZWQgPC0gZ2F0aGVyX2luZ3JlZGllbnRzKGRmLCBpbmdyZWRpZW50X2NvbG5hbWVzKSAgIyBpbmdyZWRpZW50IGNvbG5hbWVzIGRlZmluZWQgYWJvdmUgZnVuY3Rpb24KICAjIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gIyAKICAKICAjIE5leHQgd2UgZ2V0IGEgdmVjdG9yIG9mIGFsbCBpbmdyZWRpZW50IGxldmVscyBhbmQgdGFrZSBvdXQgdGhlIG9uZSB0aGF0J3MgYW4gZW1wdHkgc3RyaW5nIGFuZCAKICAjIHVzZSB0aGlzIHZlY3RvciBvZiBpbmdyZWRpZW50IGxldmVscyBpbiBgc2VsZWN0X3NwcmVhZF9jb2xzKClgIGJlbG93CgogICMgR2V0IGEgdmVjdG9yIG9mIGFsbCBpbmdyZWRpZW50IGxldmVscwogIGJlZXJfZ2F0aGVyZWQkaW5nX25hbWVzIDwtIGZhY3RvcihiZWVyX2dhdGhlcmVkJGluZ19uYW1lcykKICBpbmdyZWRpZW50X2xldmVscyA8LSBsZXZlbHMoYmVlcl9nYXRoZXJlZCRpbmdfbmFtZXMpIAogIAogICMgVGFrZSBvdXQgdGhlIGxldmVsIHRoYXQncyBqdXN0IGFuIGVtcHR5IHN0cmluZwogIHRvX2tlZXBfbGV2ZWxzIDwtICEoYygxOmxlbmd0aChpbmdyZWRpZW50X2xldmVscykpICVpbiUgd2hpY2goaW5ncmVkaWVudF9sZXZlbHMgPT0gIiIpKQogIGluZ3JlZGllbnRfbGV2ZWxzIDwtIGluZ3JlZGllbnRfbGV2ZWxzW3RvX2tlZXBfbGV2ZWxzXQogIAogIGJlZXJfZ2F0aGVyZWQkaW5nX25hbWVzIDwtIGFzLmNoYXJhY3RlcihiZWVyX2dhdGhlcmVkJGluZ19uYW1lcykKICAKICAjIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICMgCiAgCiAgIyBUaGVuIHdlIHNwcmVhZCB0aGUgaW5ncmVkaWVudCBuYW1lczogd2UgdGFrZSB3aGF0IHdhcyBwcmV2aW91c2x5IHRoZSBgdmFsdWVgIGluIG91ciBnYXRoZXJlZCBkYXRhZnJhbWUsIHRoZQogICMgYWN0dWFsIGluZ3JlZGllbnQgbmFtZXMgKENhc2NhZGUsIENlbnRlbm5pYWwpIGFuZCBtYWtlIHRoYXQgb3VyIGBrZXlgOyBpdCdsbCBmb3JtIHRoZSBuZXcgY29sdW1uIG5hbWVzLiBUaGUKICAjIG5ldyBgdmFsdWVgIGlzIGB2YWx1ZWAgaXMgY291bnQ7IGl0J2xsIHBvcHVsYXRlIHRoZSByb3cgY2VsbHMuIElmIGEgZ2l2ZW4gcm93IGhhcyBhIGNlcnRhaW4gaW5ncmVkaWVudCwgaXQKICAjIGdldHMgYSAxIGluIHRoZSBjb3JyZXNwb25kaW5nIGNlbGwsIGFuIE5BIG90aGVyd2lzZS4gCiAgIyBXZSBhZGQgYSB1bmlxdWUgaWRlbmZpdGllciBmb3IgZWFjaCByb3cgd2l0aCBgcm93YCwgd2hpY2ggd2UnbGwgZHJvcCBsYXRlciAoc2VlIFtIYWRsZXkncyBTTwogICMgY29tbWVudF0oaHR0cHM6Ly9zdGFja292ZXJmbG93LmNvbS9xdWVzdGlvbnMvMjU5NjAzOTQvdW5leHBlY3RlZC1iZWhhdmlvci13aXRoLXRpZHlyKSkuCgogIAogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSBTcHJlYWQgY29sdW1ucyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAjCiAgc3ByZWFkX2luZ3JlZGllbnRzIDwtIGZ1bmN0aW9uKGRmKSB7CiAgICBkZl9zcHJlYWQgPC0gZGYgJT4lIAogICAgICBtdXRhdGUoCiAgICAgICAgcm93ID0gMTpucm93KGRmKSAgICAgICAgIyBBZGQgYSB1bmlxdWUgaWRlbmZpdGllciBmb3IgZWFjaCByb3cgd2hpY2ggd2UnbGwgbmVlZCBpbiBvcmRlciB0byBzcHJlYWQ7IHdlJ2xsIGRyb3AgdGhpcyBsYXRlcgogICAgICApICU+JSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICBzcHJlYWQoCiAgICAgICAga2V5ID0gaW5nX25hbWVzLAogICAgICAgIHZhbHVlID0gY291bnQKICAgICAgKSAKICAgIHJldHVybihkZl9zcHJlYWQpCiAgfQogIGJlZXJfc3ByZWFkIDwtIHNwcmVhZF9pbmdyZWRpZW50cyhiZWVyX2dhdGhlcmVkKQogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAjIAoKICAKICAjIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gU2VsZWN0IG9ubHkgY2VydGFpbiBjb2x1bW5zIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gIwogIHNlbGVjdF9zcHJlYWRfY29scyA8LSBmdW5jdGlvbihkZikgewogICAgdG9fa2VlcF9jb2xfaW5kaWNlcyA8LSB3aGljaChjb2xuYW1lcyhkZikgJWluJSB0b19rZWVwX2NvbF9uYW1lcykKICAgIHRvX2tlZXBfaW5ncmVkaWVudF9pbmRpY2VzIDwtIHdoaWNoKGNvbG5hbWVzKGRmKSAlaW4lIGluZ3JlZGllbnRfbGV2ZWxzKQogICAgCiAgICB0b19rZWVwX2luZHNfYWxsIDwtIGModG9fa2VlcF9jb2xfaW5kaWNlcywgdG9fa2VlcF9pbmdyZWRpZW50X2luZGljZXMpCiAgICAKICAgIG5ld19kZiA8LSBkZiAlPiUgCiAgICAgIHNlbGVjdF8oCiAgICAgICAgLmRvdHMgPSB0b19rZWVwX2luZHNfYWxsCiAgICAgICkKICAgIHJldHVybihuZXdfZGYpCiAgfQogIGJlZXJfc3ByZWFkX3NlbGVjdGVkIDwtIHNlbGVjdF9zcHJlYWRfY29scyhiZWVyX3NwcmVhZCkKICAjIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gIyAKCiAgIyBUYWtlIG91dCBhbGwgcm93cyB0aGF0IGhhdmUgbm8gaW5ncmVkaWVudHMgc3BlY2lmaWVkIGF0IGFsbAogIGluZHNfdG9fcmVtb3ZlIDwtIGFwcGx5KGJlZXJfc3ByZWFkX3NlbGVjdGVkWywgZmlyc3RfaW5ncmVkaWVudF9pbmRleDpsYXN0X2luZ3JlZGllbnRfaW5kZXhdLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAxLCBmdW5jdGlvbih4KSBhbGwoaXMubmEoeCkpKQogIGJlZXJfc3ByZWFkX25vX25hIDwtIGJlZXJfc3ByZWFkX3NlbGVjdGVkWyAhaW5kc190b19yZW1vdmUsIF0KICAKICAKICAjIC0tLS0tLS0tLS0tLS0tLS0tIEdyb3VwIGluZ3JlZGllbnRzIGJ5IHRoZSBncm91cGVyIHNwZWNpZmllZCAtLS0tLS0tLS0tLS0tLS0tLS0tICMKICAjIFRoZW4gd2UgZG8gdGhlIGZpbmFsIHN0ZXAgYW5kIGdyb3VwIGJ5IHRoZSBncm91cGVycy4KICAKICBnZXRfaW5ncmVkaWVudHNfcGVyX2dyb3VwZXIgPC0gZnVuY3Rpb24oZGYsIGdyb3VwZXIgPSBncm91cGVyKSB7CiAgICBkZl9ncm91cGVkIDwtIGRmICU+JQogICAgICB1bmdyb3VwKCkgJT4lIAogICAgICBncm91cF9ieV8oZ3JvdXBlcikKICAgIAogICAgbm90X2Zvcl9zdW1taW5nIDwtIHdoaWNoKGNvbG5hbWVzKGRmX2dyb3VwZWQpICVpbiUgdG9fa2VlcF9jb2xfbmFtZXMpCiAgICBtYXhfbm90X2Zvcl9zdW1taW5nIDwtIG1heChub3RfZm9yX3N1bW1pbmcpCiAgICAKICAgIHBlcl9ncm91cGVyIDwtIGRmX2dyb3VwZWQgJT4lIAogICAgICBzZWxlY3QoLWMoYWJ2LCBpYnUsIHNybSkpICU+JSAgICAjIHRha2luZyBvdXQgdGVtcG9yYXJpbHkKICAgICAgc3VtbWFyaXNlX2lmKAogICAgICAgIGlzLm51bWVyaWMsICAgICAgICAgICAgICAKICAgICAgICBzdW0sIG5hLnJtID0gVFJVRQogICAgICApICU+JQogICAgICBtdXRhdGUoCiAgICAgICAgdG90YWwgPSByb3dTdW1zKC5bKG1heF9ub3RfZm9yX3N1bW1pbmcgKyAxKTpuY29sKC4pXSwgbmEucm0gPSBUUlVFKSAgICAKICAgICAgKQogICAgCiAgICAjIFNlbmQgdG90YWwgdG8gdGhlIHNlY29uZCBwb3NpdGlvbgogICAgcGVyX2dyb3VwZXIgPC0gcGVyX2dyb3VwZXIgJT4lIAogICAgICBzZWxlY3QoCiAgICAgICAgaWQsIHRvdGFsLCBldmVyeXRoaW5nKCkKICAgICAgKQogICAgCiAgICAjIFJlcGxhY2UgdG90YWwgY29sdW1uIHdpdGggbW9yZSBkZXNjcmlwdGl2ZSBuYW1lOiB0b3RhbF88aW5ncmVkaWVudD4KICAgIG5hbWVzKHBlcl9ncm91cGVyKVt3aGljaChuYW1lcyhwZXJfZ3JvdXBlcikgPT0gInRvdGFsIildIDwtIHBhc3RlMCgidG90YWxfIiwgaW5ncmVkaWVudF93YW50KQogICAgCiAgICByZXR1cm4ocGVyX2dyb3VwZXIpCiAgfQogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAjIAogIAogIGluZ3JlZGllbnRzX3Blcl9ncm91cGVyIDwtIGdldF9pbmdyZWRpZW50c19wZXJfZ3JvdXBlcihiZWVyX3NwcmVhZF9zZWxlY3RlZCwgZ3JvdXBlcikKICByZXR1cm4oaW5ncmVkaWVudHNfcGVyX2dyb3VwZXIpCn0KYGBgCgoKTm93IHJ1biB0aGUgZnVuY3Rpb24gd2l0aCBgaW5ncmVkaWVudF93YW50YCBhcyBmaXJzdCBob3BzLCBhbmQgdGhlbiBtYWx0LiBUaGVuIGpvaW4gdGhlIHJlc3VsdGluZyBkYXRhZnJhbWVzIGFuZCByZW1vdmUvcmVvcmRlciBzb21lIGNvbHVtbnMuCgpgYGB7ciwgZWNobz1UUlVFLCBldmFsPVRSVUV9CiMgUnVuIHRoZSBlbnRpcmUgZnVuY3Rpb24gd2l0aCBpbmdyZWRpZW50X3dhbnQgc2V0IHRvIGhvcHMsIGdyb3VwaW5nIGJ5IG5hbWUKaW5ncmVkaWVudHNfcGVyX2JlZXJfaG9wcyA8LSBwaWNrX2luZ3JlZGllbnRfZ2V0X2JlZXIoaW5ncmVkaWVudF93YW50ID0gImhvcHMiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYmVlcl9uZWNlc3NpdGllcywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGdyb3VwZXIgPSBjKCJpZCIpKQoKIyBTYW1lIGZvciBtYWx0CmluZ3JlZGllbnRzX3Blcl9iZWVyX21hbHQgPC0gcGlja19pbmdyZWRpZW50X2dldF9iZWVyKGluZ3JlZGllbnRfd2FudCA9ICJtYWx0IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJlZXJfbmVjZXNzaXRpZXMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBncm91cGVyID0gYygiaWQiKSkKCiMgSm9pbiB0aG9zZSBvbiBvdXIgb3JpZ2luYWwgZGF0YWZyYW1lIGJ5IG5hbWUKYmVlcl9pbmdyZWRpZW50c19qb2luX2ZpcnN0X2luZ3JlZGllbnQgPC0gbGVmdF9qb2luKGJlZXJfbmVjZXNzaXRpZXMsIGluZ3JlZGllbnRzX3Blcl9iZWVyX2hvcHMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBieSA9ICJpZCIpCmJlZXJfaW5ncmVkaWVudHNfam9pbl9hbGwgPC0gbGVmdF9qb2luKGJlZXJfaW5ncmVkaWVudHNfam9pbl9maXJzdF9pbmdyZWRpZW50LCBpbmdyZWRpZW50c19wZXJfYmVlcl9tYWx0LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ5ID0gImlkIikKCgojIFRha2Ugb3V0IHNvbWUgdW5uZWNlc3NhcnkgY29sdW1ucwp1bm5lY2Vzc2FyeV9jb2xzIDwtIGMoInN0eWxlSWQiLCAiYWJ2X3NjYWxlZCIsICJpYnVfc2NhbGVkIiwgInNybV9zY2FsZWQiLCAKICAgICAgICAgICAgICAgICAgICAgICJob3BzX2lkIiwgIm1hbHRfaWQiLCAiZ2xhc3N3YXJlSWQiLCAic3R5bGUuY2F0ZWdvcnlJZCIpCmJlZXJfaW5ncmVkaWVudHNfam9pbl9hbGwgPC0gYmVlcl9pbmdyZWRpZW50c19qb2luX2FsbFssICghIG5hbWVzKGJlZXJfaW5ncmVkaWVudHNfam9pbl9hbGwpICVpbiUgdW5uZWNlc3NhcnlfY29scyldCgoKIyBJZiB3ZSBhbHNvIHdhbnQgdG8gdGFrZSBvdXQgYW55IG9mIHRoZSBtYWx0X25hbWVfMSwgbWFsdF9uYW1lXzIsIGV0Yy4gY29sdW1ucyB3ZSBjYW4gZG8gdGhpcyB3aXRoIGEgZ3JlcAptb3JlX3VubmVjZXNzYXJ5IDwtIGMoImhvcHNfbmFtZV98bWFsdF9uYW1lXyIpCmJlZXJfaW5ncmVkaWVudHNfam9pbl9hbGwgPC0gCiAgYmVlcl9pbmdyZWRpZW50c19qb2luX2FsbFssICghIGdyZXBsKG1vcmVfdW5uZWNlc3NhcnksIG5hbWVzKGJlZXJfaW5ncmVkaWVudHNfam9pbl9hbGwpKSA9PSBUUlVFKV0KCiMgUmVvcmRlciBjb2x1bW5zIGEgYml0CmJlZXJfaW5ncmVkaWVudHNfam9pbl9hbGwgPC0gYmVlcl9pbmdyZWRpZW50c19qb2luX2FsbCAlPiUgCiAgc2VsZWN0KAogICAgaWQsIG5hbWUsIHRvdGFsX2hvcHMsIHRvdGFsX21hbHQsIGV2ZXJ5dGhpbmcoKSwgLWRlc2NyaXB0aW9uCiAgKQoKIyBLZWVwIG9ubHkgYmVlcnMgdGhhdCBmYWxsIGludG8gYSBzdHlsZV9jb2xsYXBzZWQgYnVja2V0CiMgV2UncmUgbm90IGZpbHRlcmluZyBieSBsZXZlbHMgaW4gYmVlcl9uZWNlc3NpdGllcyRzdHlsZV9jb2xsYXBzZWQgYmVjYXVzZSB0aG9zZSBsZXZlbHMgY29udGFpbiBtb3JlIHRoYW4gd2hhdCdzIGluIGp1c3QgdGhlIGtleXdvcmRzIG9mIGNvbGxhcHNlX3N0eWxlcygpCmJlZXJfaW5ncmVkaWVudHNfam9pbiA8LSBiZWVyX2luZ3JlZGllbnRzX2pvaW5fYWxsICU+JSAKICBmaWx0ZXIoCiAgICBzdHlsZV9jb2xsYXBzZWQgJWluJSBsZXZlbHMoc3R5bGVfY2VudGVycyRzdHlsZV9jb2xsYXBzZWQpCiAgKSAlPiUgCiAgZHJvcGxldmVscygpCgojIEFuZCBnZXQgYSBkZiB0aGF0IGluY2x1ZGVzIHRvdGFsX2hvcHMgYW5kIHRvdGFsX21hbHQgYnV0IG5vdCBhbGwgdGhlIG90aGVyIGluZ3JlZGllbnQgY29sdW1ucwpiZWVyX3RvdGFsc19hbGwgPC0gYmVlcl9pbmdyZWRpZW50c19qb2luX2FsbCAlPiUgCiAgc2VsZWN0KAogICAgaWQsIG5hbWUsIHRvdGFsX2hvcHMsIHRvdGFsX21hbHQsIHN0eWxlLCBzdHlsZV9jb2xsYXBzZWQsCiAgICBhYnYsIGlidSwgc3JtLCBnbGFzcywgaG9wc19uYW1lLCBtYWx0X25hbWUKICApCgojIEFuZCBqdXN0IHN0eWxlcyBpbiBzdHlsZV9jb2xsYXBzZWQKYmVlcl90b3RhbHMgPC0gYmVlcl9pbmdyZWRpZW50c19qb2luICU+JSAKICBmaWx0ZXIoCiAgICBzdHlsZV9jb2xsYXBzZWQgJWluJSBsZXZlbHMoc3R5bGVfY2VudGVycyRzdHlsZV9jb2xsYXBzZWQpCiAgKSAlPiUgCiAgZHJvcGxldmVscygpCgpgYGAKCgpOb3cgd2UncmUgbGVmdCB3aXRoIHNvbWV0aGluZyBvZiBhIHNwYXJzZSBtYXRyaXggb2YgYWxsIHRoZSBpbmdyZWRpZW50cyBjb21wYXJlZCB0byBhbGwgdGhlIGJlZXJzCmBgYHtyLCBlY2hvPVRSVUV9CmthYmxlKGJlZXJfaW5ncmVkaWVudHNfam9pblsxOjIwLCBdKQpgYGAKCgoKKioqCgpOb3cgdGhhdCB0aGUgbXVuZ2luZyBpcyBkb25lLCBvbnRvIHRoZSBtYWluIHF1ZXN0aW9uOiBkbyBuYXR1cmFsIGNsdXN0ZXJzIGluIGJlZXIgYWxpZ24gd2l0aCBzdHlsZSBib3VuZGFyaWVzPwoKCioqKgoKIyMjIFVuc3VwZXJ2aXNlZCBDbHVzdGVyaW5nIApXZSBLLW1lYW5zIGNsdXN0ZXIgYmVlcnMgYmFzZWQgb24gY2VydGFpbiBudW1lcmljIHByZWRpY3RvciB2YXJpYWJsZXMuIAoKCioqUHJlcCoqCgoqIFdyaXRlIGEgZnVuY2l0b24gdGhhdCB0YWtlcyBhIGRhdGFmcmFtZSwgYSBzZXQgb2YgcHJlZGljdG9ycywgYSByZXNwb25zZSB2YXJpYWJsZSwgYW5kIHRoZSBudW1iZXIgb2YgY2x1c3RlciBjZW50ZXJzIHlvdSB3YW50CiAgICAqIE5COiBUaGVyZSBhcmUgbm90IG5vdCB2ZXJ5IG1hbnkgYmVlcnMgaGF2ZSBTUk0gc28gd2UgbWF5IG5vdCB3YW50IHRvIG9taXQgYmFzZWQgb24gaXQKCiogVGFrZSBvdXQgbWlzc2luZyB2YWx1ZXMsIGFuZCBzY2FsZSB0aGUgZGF0YQoqIFRha2Ugb3V0IG91dGxpZXJzLCBkZWZpbmVkIGFzIGJlZXJzIGhhdmUgdG8gaGF2ZSBhbiBBQlYgYmV0d2VlbiAzIGFuZCAyMCBhbmQgYW4gSUJVIGxlc3MgdGhhbiAyMDAKKiBUaGVuIGNsdXN0ZXIgb24ganVzdCB0aGUgcHJlZGljdG9ycyBhbmQgY29tcGFyZSB0byB0aGUgcmVzcG9uc2UgdmFyaWFibGUKICAKCmBgYHtyLCBlY2hvPVRSVUV9CmxpYnJhcnkoTmJDbHVzdCkKCnByZXBfY2x1c3RlcnMgPC0gZnVuY3Rpb24oZGYsIHByZWRzLCB0b19zY2FsZSwgcmVzcCkgewogIGRmX2Zvcl9jbHVzdGVyaW5nIDwtIGRmICU+JQogICAgc2VsZWN0XyguZG90cyA9IGMocmVzcG9uc2VfdmFycywgY2x1c3Rlcl9vbikpICU+JQogICAgbmEub21pdCgpICU+JQogICAgZmlsdGVyKAogICAgICBhYnYgPCAyMCAmIGFidiA+IDMgICAgIyBPbmx5IGtlZXAgYmVlcnMgd2l0aCBBQlYgYmV0d2VlbiAzIGFuZCAyMCBhbmQgYW4gSUJVIGxlc3MgdGhhbiAyMDAKICAgICkgJT4lCiAgICBmaWx0ZXIoCiAgICAgIGlidSA8IDIwMCAgICAKICAgICkKICAKICBkZl9hbGxfcHJlZHMgPC0gZGZfZm9yX2NsdXN0ZXJpbmcgJT4lCiAgICBzZWxlY3RfKC5kb3RzID0gcHJlZHMpCiAgCiAgZGZfcHJlZHNfc2NhbGUgPC0gZGZfYWxsX3ByZWRzICU+JQogICAgc2VsZWN0XyguZG90cyA9IHRvX3NjYWxlKSAlPiUKICAgIHJlbmFtZSgKICAgICAgYWJ2X3NjYWxlZCA9IGFidiwKICAgICAgaWJ1X3NjYWxlZCA9IGlidSwKICAgICAgc3JtX3NjYWxlZCA9IHNybQogICAgKSAlPiUKICAgIHNjYWxlKCkgJT4lCiAgICBhc190aWJibGUoKQogIAogIGRmX3ByZWRzIDwtIGJpbmRfY29scyhkZl9wcmVkc19zY2FsZSwgZGZfYWxsX3ByZWRzWywgKCFuYW1lcyhkZl9hbGxfcHJlZHMpICVpbiUgdG9fc2NhbGUpXSkKICAKICBkZl9vdXRjb21lIDwtIGRmX2Zvcl9jbHVzdGVyaW5nICU+JQogICAgc2VsZWN0XyguZG90cyA9IHJlc3ApICU+JQogICAgbmEub21pdCgpCiAgCiAgY2x1c3Rlcl9wcmVwX291dCA8LSBsaXN0KGRmX2Zvcl9jbHVzdGVyaW5nID0gZGZfZm9yX2NsdXN0ZXJpbmcsIHByZWRzID0gZGZfcHJlZHMsIG91dGNvbWUgPSBkZl9vdXRjb21lKQogIAogIHJldHVybihjbHVzdGVyX3ByZXBfb3V0KQp9CmBgYAoKClByZXAgdGhlIGNsdXN0ZXJzLiBXZSdsbCBjbHVzdGVyIG9uIHRoZSBwcmVkaWN0b3JzIEFCViwgSUJVLCBTUk0sIHRvdGFsIG51bWJlciBvZiBob3BzLCBhbmQgdG90YWwgbnVtYmVyIG9mIG1hbHRzLgpgYGB7ciwgZWNobz1UUlVFfQpjbHVzdGVyX29uIDwtIGMoImFidiIsICJpYnUiLCAic3JtIiwgInRvdGFsX2hvcHMiLCAidG90YWxfbWFsdCIpCnRvX3NjYWxlIDwtIGMoImFidiIsICJpYnUiLCAic3JtIiwgInRvdGFsX2hvcHMiLCAidG90YWxfbWFsdCIpCnJlc3BvbnNlX3ZhcnMgPC0gYygibmFtZSIsICJzdHlsZSIsICJzdHlsZV9jb2xsYXBzZWQiKQoKY2x1c3Rlcl9wcmVwIDwtIHByZXBfY2x1c3RlcnMoZGYgPSBiZWVyX3RvdGFscywKICAgICAgICAgICAgICAgICAgIHByZWRzID0gY2x1c3Rlcl9vbiwKICAgICAgICAgICAgICAgICAgIHRvX3NjYWxlID0gdG9fc2NhbGUsCiAgICAgICAgICAgICAgICAgICByZXNwID0gcmVzcG9uc2VfdmFycykKCmBgYAoKCgoKRGV0ZXJtaW5lIGFuIG9wdGltYWwgbnVtYmVyIG9mIGNsdXN0ZXJzIGJ5IHNldHRpbmcgdGhlIG1pbmltdW0gdG8gMiBhbmQgbWF4IHRvIDE1IGNsdXN0ZXJzLgpGcm9tIHRoZSByZXN1bHRpbmcgaGlzdG9ncmFtLCAxMCBzZWVtZWQgYW4gb3B0aW1hbCBudW1iZXIgb2YgY2x1c3RlcnMuIAoKYGBge3IsIGVjaG89VFJVRSwgZXZhbD1GQUxTRX0KbmIgPC0gTmJDbHVzdChjbHVzdGVyX3ByZXAkcHJlZHMsIGRpc3RhbmNlID0gImV1Y2xpZGVhbiIsCiAgICAgICAgICAgICAgbWluLm5jID0gMiwgbWF4Lm5jID0gMTUsIG1ldGhvZCA9ICJrbWVhbnMiKQpoaXN0KG5iJEJlc3QubmNbMSxdLCBicmVha3MgPSBtYXgobmEub21pdChuYiRCZXN0Lm5jWzEsXSkpKQpgYGAKCgoKCiAKKipDbHVzdGVyKioKCk5vdyBjbHVzdGVyIG9uIHRoZSBwcmVwZWQgcHJlZGljdG9ycyB3aXRoIDEwIGNlbnRlcnMuCgpgYGB7ciwgZWNobz1UUlVFfQpjbHVzdGVyX2l0IDwtIGZ1bmN0aW9uKGRmX3ByZWRzLCBuX2NlbnRlcnMpIHsKICBzZXQuc2VlZCg5KQogIGNsdXN0ZXJlZF9kZl9vdXQgPC0ga21lYW5zKHggPSBkZl9wcmVkcyRwcmVkcywgY2VudGVycyA9IG5fY2VudGVycywgdHJhY2UgPSBGQUxTRSkKCiAgY2x1c3RlcmVkX2RmIDwtIGFzX3RpYmJsZShkYXRhLmZyYW1lKAogICAgY2x1c3Rlcl9hc3NpZ25tZW50ID0gZmFjdG9yKGNsdXN0ZXJlZF9kZl9vdXQkY2x1c3RlciksCiAgICBkZl9wcmVkcyRvdXRjb21lLCBkZl9wcmVkcyRwcmVkcywKICAgIGRmX3ByZWRzJGRmX2Zvcl9jbHVzdGVyaW5nICU+JSBzZWxlY3QoYWJ2LCBpYnUsIHNybSkpKQoKICByZXR1cm4oY2x1c3RlcmVkX2RmKQp9CgpjbHVzdGVyZWRfYmVlciA8LSBjbHVzdGVyX2l0KGRmX3ByZWRzID0gY2x1c3Rlcl9wcmVwLCBuX2NlbnRlcnMgPSAxMCkKCmBgYAoKCkhlYWQgb2YgdGhlIHJlc3VsdGluZyBjbHVzdGVyZWQgZGF0YS4gQ2x1c3RlciBhc3NpZ25tZW50IGNvbHVtbiBvbiB0aGUgZmFyIGxlZnQuCmBgYHtyLCBlY2hvPVRSVUV9CmthYmxlKGNsdXN0ZXJlZF9iZWVyWzE6MjAsIF0pCgojIEhvdyBtYW55IHJvd3MgZG8gd2UgaGF2ZT8KbnJvdyhjbHVzdGVyZWRfYmVlcikKYGBgCgpKb2luIHRoZSBjbHVzdGVyZWQgYmVlciBvbiBgYmVlcl9pbmdyZWRpZW50c19qb2luYApgYGB7cn0KYmVlcl9pbmdyZWRpZW50c19qb2luX2NsdXN0ZXJlZCA8LSBsZWZ0X2pvaW4oYmVlcl9pbmdyZWRpZW50c19qb2luLCBjbHVzdGVyZWRfYmVlciwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ5ID0gIm5hbWUiKQpgYGAKCgpBIHRhYmxlIG9mIGNsdXN0ZXIgY291bnRzIGJyb2tlbiBkb3duIGJ5IHN0eWxlCmBgYHtyfQpjbHVzdGVyX3RhYmxlX2NvdW50cyA8LSB0YWJsZShzdHlsZSA9IGNsdXN0ZXJlZF9iZWVyJHN0eWxlX2NvbGxhcHNlZCwgY2x1c3RlciA9IGNsdXN0ZXJlZF9iZWVyJGNsdXN0ZXJfYXNzaWdubWVudCkKCmthYmxlKGNsdXN0ZXJfdGFibGVfY291bnRzKQpgYGAKCgpQbG90IHRoZSBjbHVzdGVycy4gVGhlcmUgYXJlIDMgYXhlczogQUJWLCBJQlUsIGFuZCBTUk0sIHNvIHdlIGNob29zZSB0d28gYXQgYSB0aW1lLiAKCmBgYHtyLCBlY2hvPVRSVUV9CmNsdXN0ZXJlZF9iZWVyX3Bsb3RfYWJ2X2lidSA8LSBnZ3Bsb3QoZGF0YSA9IGNsdXN0ZXJlZF9iZWVyLCBhZXMoeCA9IGFidiwgeSA9IGlidSwgY29sb3VyID0gY2x1c3Rlcl9hc3NpZ25tZW50KSkgKyAKICBnZW9tX2ppdHRlcigpICsgdGhlbWVfbWluaW1hbCgpICArCiAgZ2d0aXRsZSgiay1NZWFucyBDbHVzdGVyaW5nIG9mIEJlZXIgYnkgQUJWLCBJQlUsIFNSTSIpICsKICBsYWJzKHggPSAiQUJWIiwgeSA9ICJJQlUiKSArCiAgbGFicyhjb2xvdXIgPSAiQ2x1c3RlciBBc3NpZ25tZW50IikKY2x1c3RlcmVkX2JlZXJfcGxvdF9hYnZfaWJ1CgpjbHVzdGVyZWRfYmVlcl9wbG90X2Fidl9zcm0gPC0gZ2dwbG90KGRhdGEgPSBjbHVzdGVyZWRfYmVlciwgYWVzKHggPSBhYnYsIHkgPSBzcm0sIGNvbG91ciA9IGNsdXN0ZXJfYXNzaWdubWVudCkpICsgCiAgZ2VvbV9qaXR0ZXIoKSArIHRoZW1lX21pbmltYWwoKSAgKwogIGdndGl0bGUoImstTWVhbnMgQ2x1c3RlcmluZyBvZiBCZWVyIGJ5IEFCViwgSUJVLCBTUk0iKSArCiAgbGFicyh4ID0gIkFCViIsIHkgPSAiU1JNIikgKwogIGxhYnMoY29sb3VyID0gIkNsdXN0ZXIgQXNzaWdubWVudCIpCmNsdXN0ZXJlZF9iZWVyX3Bsb3RfYWJ2X3NybQpgYGAKCgoKTm93IHdlIGNhbiBhZGQgaW4gdGhlIHN0eWxlIGNlbnRlcnMgKG1lYW5zKSBmb3IgZWFjaCBgc3R5bGVfY29sbGFwc2VkYCBhbmQgbGFiZWwgaXQuCgpgYGB7ciwgZWNobz1UUlVFfQpsaWJyYXJ5KGdncmVwZWwpCmFidl9pYnVfY2x1c3RlcnNfdnNfc3R5bGVfY2VudGVycyA8LSBnZ3Bsb3QoKSArICAgCiAgZ2VvbV9wb2ludChkYXRhID0gY2x1c3RlcmVkX2JlZXIsIAogICAgICAgICAgICAgYWVzKHggPSBhYnYsIHkgPSBpYnUsIGNvbG91ciA9IGNsdXN0ZXJfYXNzaWdubWVudCksIGFscGhhID0gMC41KSArCiAgZ2VvbV9wb2ludChkYXRhID0gc3R5bGVfY2VudGVycywKICAgICAgICAgICAgIGFlcyhtZWFuX2FidiwgbWVhbl9pYnUpLCBjb2xvdXIgPSAiYmxhY2siKSArCiAgZ2VvbV90ZXh0X3JlcGVsKGRhdGEgPSBzdHlsZV9jZW50ZXJzLCBhZXMobWVhbl9hYnYsIG1lYW5faWJ1LCBsYWJlbCA9IHN0eWxlX2NvbGxhcHNlZCksIAogICAgICAgICAgICAgICAgICBib3gucGFkZGluZyA9IHVuaXQoMC40NSwgImxpbmVzIiksCiAgICAgICAgICAgICAgICAgIGZhbWlseSA9ICJDYWxpYnJpIiwKICAgICAgICAgICAgICAgICAgbGFiZWwuc2l6ZSA9IDAuMykgKwogIGdndGl0bGUoIlBvcHVsYXIgU3R5bGVzIHZzLiBrLU1lYW5zIENsdXN0ZXJpbmcgb2YgQmVlciBieSBBQlYsIElCVSwgU1JNIikgKwogIGxhYnMoeCA9ICJBQlYiLCB5ID0gIklCVSIpICsKICBsYWJzKGNvbG91ciA9ICJDbHVzdGVyIEFzc2lnbm1lbnQiKSArCiAgdGhlbWVfYncoKQphYnZfaWJ1X2NsdXN0ZXJzX3ZzX3N0eWxlX2NlbnRlcnMKYGBgCgoKVGhlIGNsdXN0ZXJpbmcgYWJvdmUgdXNlZCBhIHNtYWxsZXIgbnVtYmVyIG9mIGNsdXN0ZXJzICgxMCkgdGhhbiB0aGVyZSBhcmUgYHN0eWxlc19jb2xsYXBzZWRgLiBUaGF0IG1ha2VzIGl0IGRpZmZpY3VsdCB0byBkZXRlcm1pbmUgd2hldGhlciBhIGdpdmVuIHN0eWxlIGZpdHMgc251Z2x5IGludG8gYSBjbHVzdGVyIG9yIG5vdC4KCgoKKipDbHVzdGVyIG9uIGp1c3QgY2VydGFpbiBzZWxlY3RlZCBzdHlsZXMqKgoKV2UnbGwgdGFrZSBmaXZlIHZlcnkgZGlzdGluY3QgY29sbGFwc2VkIHN0eWxlcyBhbmQgcmUtcnVuIHRoZSBjbHVzdGVyaW5nIG9uIGJlZXJzIHRoYXQgZmFsbCBpbnRvIHRoZXNlIGNhdGVnb3JpZXMuIApUaGVzZSBzdHlsZXMgd2VyZSBpbnRlbnRpb25hbGx5IGNob3NlbiBiZWNhdXNlIHRoZXkgYXJlIHF1aXRlIGRpc3RpbmN0OiBCbG9uZGUsIElQQSwgU3RvdXQsIFRyaXBlbCwgV2hlYXQuIEFyZ3VhYmx5LCBvZiB0aGVzZSBmaXZlIHN0eWxlcyBCbG9uZGVzIGFuZCBXaGVhdHMgYXJlIHRoZSBjbG9zZXN0CgoKCmBgYHtyLCBlY2hvPVRSVUV9CnN0eWxlc190b19rZWVwIDwtIGMoIkJsb25kZSIsICJJbmRpYSBQYWxlIEFsZSIsICJTdG91dCIsICJUcmlwZWwiLCAiV2hlYXQiKQpidF9jZXJ0YWluX3N0eWxlcyA8LSBiZWVyX3RvdGFscyAlPiUKICBmaWx0ZXIoCiAgICBzdHlsZV9jb2xsYXBzZWQgJWluJSBzdHlsZXNfdG9fa2VlcAogICkgJT4lIAogIGRyb3BsZXZlbHMoKQoKY2x1c3Rlcl9vbiA8LSBjKCJhYnYiLCAiaWJ1IiwgInNybSIsICJ0b3RhbF9ob3BzIiwgInRvdGFsX21hbHQiKQp0b19zY2FsZSA8LSBjKCJhYnYiLCAiaWJ1IiwgInNybSIsICJ0b3RhbF9ob3BzIiwgInRvdGFsX21hbHQiKQpyZXNwb25zZV92YXJzIDwtIGMoIm5hbWUiLCAic3R5bGUiLCAic3R5bGVfY29sbGFwc2VkIikKCmJ0X2NsdXN0ZXJfcHJlcCA8LSBwcmVwX2NsdXN0ZXJzKGRmID0gYnRfY2VydGFpbl9zdHlsZXMsCiAgICAgICAgICAgICAgICAgICBwcmVkcyA9IGNsdXN0ZXJfb24sCiAgICAgICAgICAgICAgICAgICB0b19zY2FsZSA9IHRvX3NjYWxlLAogICAgICAgICAgICAgICAgICAgcmVzcCA9IHJlc3BvbnNlX3ZhcnMpCgpjZXJ0YWluX3N0eWxlc19jbHVzdGVyZWQgPC0gY2x1c3Rlcl9pdChkZl9wcmVkcyA9IGJ0X2NsdXN0ZXJfcHJlcCwgbl9jZW50ZXJzID0gNSkKCnN0eWxlX2NlbnRlcnNfY2VydGFpbl9zdHlsZXMgPC0gc3R5bGVfY2VudGVycyAlPiUgCiAgZmlsdGVyKHN0eWxlX2NvbGxhcHNlZCAlaW4lIHN0eWxlc190b19rZWVwKQpgYGAKCgoKCgpUYWJsZSBvZiBzdHlsZSB2cy4gY2x1c3Rlci4KYGBge3IsIGVjaG89VFJVRX0Ka2FibGUodGFibGUoc3R5bGUgPSBjZXJ0YWluX3N0eWxlc19jbHVzdGVyZWQkc3R5bGVfY29sbGFwc2VkLCBjbHVzdGVyID0gY2VydGFpbl9zdHlsZXNfY2x1c3RlcmVkJGNsdXN0ZXJfYXNzaWdubWVudCkpCmBgYAoKCgpOb3cgdGhhdCB3ZSBoYXZlIGEgbWFuYWdlYWJsZSBudW1iZXIgb2Ygc3R5bGVzLCB3ZSBjYW4gc2VlIGhvdyB3ZWxsIGZpdCBlYWNoIGNsdXN0ZXIgaXMgdG8gZWFjaCBzdHlsZS4gSWYgdGhlIGZlYXR1cmVzIHdlIGNsdXN0ZXJlZCBvbiBwZXJmZWN0bHkgcHJlZGljdGVkIHN0eWxlLCB0aGVyZSB3b3VsZCBlYWNoIGNvbG9yIChjbHVzdGVyKSB3b3VsZCBiZSB1bmlxdWUgdG8gZWFjaCBmYWNldCBvZiB0aGUgcGxvdC4gKEUuZy4sIGxlZnQgZW50aXJlbHkgYmx1ZSwgc2Vjb25kIGZyb20gbGVmdCBlbnRpcmVseSBncmVlbiwgZXRjLikKCgoKYGBge3IsIGVjaG89VFJVRX0KYnlfc3R5bGVfcGxvdCA8LSBnZ3Bsb3QoKSArICAgCiAgZ2VvbV9wb2ludChkYXRhID0gY2VydGFpbl9zdHlsZXNfY2x1c3RlcmVkLCAKICAgICAgICAgICAgIGFlcyh4ID0gYWJ2LCB5ID0gaWJ1LAogICAgICAgICAgICAgICAgIGNvbG91ciA9IGNsdXN0ZXJfYXNzaWdubWVudCksIGFscGhhID0gMC41KSArCiAgZmFjZXRfZ3JpZCguIH4gc3R5bGVfY29sbGFwc2VkKSArCiAgZ2VvbV9wb2ludChkYXRhID0gc3R5bGVfY2VudGVyc19jZXJ0YWluX3N0eWxlcywKICAgICAgICAgICBhZXMobWVhbl9hYnYsIG1lYW5faWJ1KSwgY29sb3VyID0gImJsYWNrIiwgc2hhcGUgPSA1KSArCiAgZ2d0aXRsZSgiU2VsZWN0ZWQgU3R5bGVzIENsdXN0ZXIgQXNzaWdubWVudCIpICsKICBsYWJzKHggPSAiQUJWIiwgeSA9ICJJQlUiKSArCiAgbGFicyhjb2xvdXIgPSAiQ2x1c3RlciIpICsKICB0aGVtZV9idygpCmJ5X3N0eWxlX3Bsb3QKYGBgCgoKCjwhLS0gZ2dwbG90KCkgKyAtLT4KPCEtLSAgIGdlb21fcG9pbnQoZGF0YSA9IGNlcnRhaW5fc3R5bGVzX2NsdXN0ZXJlZCwgLS0+CjwhLS0gICAgICAgICAgICAgIGFlcyh4ID0gYWJ2LCB5ID0gaWJ1LCAtLT4KPCEtLSAgICAgICAgICAgICAgICAgIHNoYXBlID0gY2x1c3Rlcl9hc3NpZ25tZW50LCAtLT4KPCEtLSAgICAgICAgICAgICAgICAgIGNvbG91ciA9IHN0eWxlX2NvbGxhcHNlZCksIGFscGhhID0gMC41KSArIC0tPgo8IS0tICAgZ2VvbV9wb2ludChkYXRhID0gc3R5bGVfY2VudGVyc19jZXJ0YWluX3N0eWxlcywgLS0+CjwhLS0gICAgICAgICAgICAgIGFlcyhtZWFuX2FidiwgbWVhbl9pYnUpLCBjb2xvdXIgPSAiYmxhY2siKSArIC0tPgo8IS0tICAgZ2VvbV90ZXh0X3JlcGVsKGRhdGEgPSBzdHlsZV9jZW50ZXJzX2NlcnRhaW5fc3R5bGVzLCAtLT4KPCEtLSAgICAgICAgICAgICAgICAgICBhZXMobWVhbl9hYnYsIG1lYW5faWJ1LCBsYWJlbCA9IHN0eWxlX2NvbGxhcHNlZCksIC0tPgo8IS0tICAgICAgICAgICAgICAgICAgIGJveC5wYWRkaW5nID0gdW5pdCgwLjQ1LCAibGluZXMiKSwgLS0+CjwhLS0gICAgICAgICAgICAgICAgICAgZmFtaWx5ID0gIkNhbGlicmkiLCAtLT4KPCEtLSAgICAgICAgICAgICAgICAgICBsYWJlbC5zaXplID0gMC4zKSArIC0tPgo8IS0tICAgZ2d0aXRsZSgiU2VsZWN0ZWQgU3R5bGVzIChjb2xvcnMpIG1hdGNoZWQgd2l0aCBDbHVzdGVyIEFzc2lnbm1lbnRzIChzaGFwZXMpIikgKyAtLT4KPCEtLSAgIGxhYnMoeCA9ICJBQlYiLCB5ID0gIklCVSIpICsgLS0+CjwhLS0gICBsYWJzKGNvbG91ciA9ICJTdHlsZSIsIHNoYXBlID0gIkNsdXN0ZXIgQXNzaWdubWVudCIpICsgLS0+CjwhLS0gICB0aGVtZV9idygpIC0tPgoKPCEtLSBgYGAgLS0+CgoKCiMjIFJhbmRvbSBhc2lkZXMgaW50byBob3BzCgoqKkRvIG1vcmUgaG9wcyBhbHdheXMgbWVhbiBtb3JlIGJpdHRlcm5lc3M/KioKCiogSXQgd291bGQgYXBwZWFyIHNvLCBmcm9tIHRoaXMgZ3JhcGggKGNvbnNpZGVyaW5nIG9ubHkgYmVlciBpbiB0aGUgbW9zdCBwb3B1bGFyIHN0eWxlcykgYW5kIHRoaXMgcmVncmVzc2lvbiAoYmV0YSA9IDIuMzk0NDE4KQpgYGB7ciwgZWNobz1UUlVFfQpnZ3Bsb3QoZGF0YSA9IGJlZXJfaW5ncmVkaWVudHNfam9pbiwgYWVzKHRvdGFsX2hvcHMsIGlidSkpICsKICBnZW9tX3BvaW50KGFlcyh0b3RhbF9ob3BzLCBpYnUsIGNvbG91ciA9IHN0eWxlX2NvbGxhcHNlZCkpICsKICBnZW9tX3Ntb290aChtZXRob2QgPSBsbSwgc2UgPSBGQUxTRSwgY29sb3VyID0gImJsYWNrIikgKyAKICBnZ3RpdGxlKCJIb3BzIFBlciBCZWVyIHZzLiBCaXR0ZXJuZXNzIikgKwogIGxhYnMoeCA9ICJOdW1iZXIgb2YgSG9wcyIsIHkgPSAiSUJVIiwgY29sb3VyID0gIlN0eWxlIENvbGxhcHNlZCIpICsKICB0aGVtZV9taW5pbWFsKCkKCmBgYAoKClJlZ3Jlc3NpbmcgdG90YWwgbnVtYmVyIG9mIGhvcHMgb24gYml0dGVybmVzcyAoSUJVKToKYGBge3IsIGVjaG8gPSBUUlVFfQpob3BzX2lidV9sbSA8LSBsbShpYnUgfiB0b3RhbF9ob3BzLCBkYXRhID0gYmVlcl9pbmdyZWRpZW50c19qb2luKQpicm9vbTo6dGlkeShob3BzX2lidV9sbSkKYGBgCgoKVGhpcyBob2xkcyBldmVuIHdpdGggNSBvciBtb3JlIGhvcHMuCmBgYHtyLCBlY2hvPVRSVUV9CmdncGxvdChkYXRhID0gYmVlcl9pbmdyZWRpZW50c19qb2luW3doaWNoKGJlZXJfaW5ncmVkaWVudHNfam9pbiR0b3RhbF9ob3BzID49IDUpLCBdLCBhZXModG90YWxfaG9wcywgaWJ1KSkgKwogIGdlb21fcG9pbnQoYWVzKHRvdGFsX2hvcHMsIGlidSwgY29sb3VyID0gc3R5bGVfY29sbGFwc2VkKSkgKwogIGdlb21fc21vb3RoKG1ldGhvZCA9IGxtLCBzZSA9IEZBTFNFLCBjb2xvdXIgPSAiYmxhY2siKSArCiAgZ2d0aXRsZSgiMysgSG9wcyBQZXIgQmVlciB2cy4gQml0dGVybmVzcyIpICsKICBsYWJzKHggPSAiTnVtYmVyIG9mIEhvcHMiLCB5ID0gIklCVSIsIGNvbG91ciA9ICJTdHlsZSBDb2xsYXBzZWQiKSArCiAgdGhlbWVfbWluaW1hbCgpCmBgYAoKYGBge3IsIGVjaG8gPSBUUlVFfQpmaXZlX3BsdXNfaG9wc19pYnVfbG0gPC0gbG0oaWJ1IH4gdG90YWxfaG9wcywgZGF0YSA9IGJlZXJfaW5ncmVkaWVudHNfam9pblt3aGljaChiZWVyX2luZ3JlZGllbnRzX2pvaW4kdG90YWxfaG9wcyA+IDUpLCBdKQpicm9vbTo6dGlkeShmaXZlX3BsdXNfaG9wc19pYnVfbG0pCmBgYAoKKipNb3N0IHBvcHVsYXIgaG9wcyoqCgpgYGB7ciwgZWNobz1UUlVFfQojIEdhdGhlciB1cCBhbGwgdGhlIGhvcHMgY29sdW1ucyBpbnRvIG9uZSBjYWxsZWQgYGhvcF9uYW1lYApiZWVyX25lY2Vzc2l0aWVzX2hvcHNfZ2F0aGVyZWQgPC0gYmVlcl9uZWNlc3NpdGllcyAlPiUKICBnYXRoZXIoCiAgICBob3Bfa2V5LCBob3BfbmFtZSwgaG9wc19uYW1lXzE6aG9wc19uYW1lXzEzCiAgKSAlPiUgYXNfdGliYmxlKCkKCiMgRmlsdGVyIHRvIGp1c3QgdGhvc2UgYmVlcnMgdGhhdCBoYXZlIGF0IGxlYXN0IG9uZSBob3AKYmVlcl9uZWNlc3NpdGllc193X2hvcHMgPC0gYmVlcl9uZWNlc3NpdGllc19ob3BzX2dhdGhlcmVkICU+JSAKICBmaWx0ZXIoIWlzLm5hKGhvcF9uYW1lKSkgJT4lIAogIGZpbHRlcighaG9wX25hbWUgPT0gIiIpCgpiZWVyX25lY2Vzc2l0aWVzX3dfaG9wcyRob3BfbmFtZSA8LSBmYWN0b3IoYmVlcl9uZWNlc3NpdGllc193X2hvcHMkaG9wX25hbWUpCgojIEZvciBhbGwgaG9wcywgZmluZCB0aGUgbnVtYmVyIG9mIGJlZXJzIHRoZXkncmUgaW4gYXMgd2VsbCBhcyB0aG9zZSBiZWVycycgbWVhbiBJQlUgYW5kIEFCVgpob3BzX2JlZXJfc3RhdHMgPC0gYmVlcl9uZWNlc3NpdGllc193X2hvcHMgJT4lIAogIHVuZ3JvdXAoKSAlPiUgCiAgZ3JvdXBfYnkoaG9wX25hbWUpICU+JSAKICBzdW1tYXJpc2UoCiAgICBtZWFuX2lidSA9IG1lYW4oaWJ1LCBuYS5ybSA9IFRSVUUpLCAKICAgIG1lYW5fYWJ2ID0gbWVhbihhYnYsIG5hLnJtID0gVFJVRSksCiAgICBuID0gbigpCiAgKSAlPiUgCiAgYXJyYW5nZShkZXNjKG4pKQoKIyBQYXJlIHRvIGhvcHMgdGhhdCBhcmUgdXNlZCBpbiBhdCBsZWFzdCA1MCBiZWVycwpwb3BfaG9wc19iZWVyX3N0YXRzIDwtIGhvcHNfYmVlcl9zdGF0c1tob3BzX2JlZXJfc3RhdHMkbiA+IDUwLCBdIAoKcG9wX2hvcHNfZGlzcGxheSA8LSBwb3BfaG9wc19iZWVyX3N0YXRzICU+JSAKICAgIHJlbmFtZSgKICAgIGBIb3AgTmFtZWAgPSBob3BfbmFtZSwKICAgIGBNZWFuIElCVWAgPSBtZWFuX2lidSwKICAgIGBNZWFuIEFCVmAgPSBtZWFuX2FidiwKICAgIGBOdW1iZXIgQmVlcnMgQ29udGFpbmluZyB0aGlzIEhvcGAgPSBuCiAgKQoKa2FibGUocG9wX2hvcHNfZGlzcGxheSkKCiMgS2VlcCBqdXN0IGJlZXJzIHRoYXQgY29udGFpbiB0aGVzZSBtb3N0IHBvcHVsYXIgaG9wcwpiZWVyX25lY2Vzc2l0aWVzX3dfcG9wdWxhcl9ob3BzIDwtIGJlZXJfbmVjZXNzaXRpZXNfd19ob3BzICU+JSAKICBmaWx0ZXIoaG9wX25hbWUgJWluJSBwb3BfaG9wc19iZWVyX3N0YXRzJGhvcF9uYW1lKSAlPiUgCiAgZHJvcGxldmVscygpIApgYGAKCkFyZSB0aGVyZSBjZXJ0aWFuIGhvcHMgdGhhdCBhcmUgdXNlZCBtb3JlIG9mdGVuIGluIHZlcnkgaGlnaCBJQlUgb3IgQUJWIGJlZXJzPwpIYXJkIHRvIGRldGVjdCBhIHBhdHRlcm4KYGBge3IsIGVjaG8gPSBUUlVFfQpnZ3Bsb3QoZGF0YSA9IGJlZXJfbmVjZXNzaXRpZXNfd19wb3B1bGFyX2hvcHMpICsgCiAgZ2VvbV9wb2ludChhZXMoYWJ2LCBpYnUsIGNvbG91ciA9IGhvcF9uYW1lKSkgKwogIGdndGl0bGUoIkJlZXJzIENvbnRhaW5pbmcgbW9zdCBQb3B1bGFyIEhvcHMiKSArCiAgbGFicyh4ID0gIkFCViIsIHkgPSAiSUJVIiwgY29sb3VyID0gIkhvcCBOYW1lIikgKwogIHRoZW1lX21pbmltYWwoKQpgYGAKCmBgYHtyLCBlY2hvPVRSVUV9CmdncGxvdChkYXRhID0gcG9wX2hvcHNfYmVlcl9zdGF0cykgKyAKICBnZW9tX3BvaW50KGFlcyhtZWFuX2FidiwgbWVhbl9pYnUsIGNvbG91ciA9IGhvcF9uYW1lLCBzaXplID0gbikpICsKICBnZ3RpdGxlKCJNb3N0IFBvcHVsYXIgSG9wcycgRWZmZWN0IG9uIEFsY29ob2wgYW5kIEJpdHRlcm5lc3MiKSArCiAgbGFicyh4ID0gIk1lYW4gQUJWIHBlciBIb3AgVHlwZSIsIHkgPSAiTWVhbiBJQlUgcGVyIEhvcCBUeXBlIiwgY29sb3VyID0gIkhvcCBOYW1lIiwgCiAgICAgICBzaXplID0gIk51bWJlciBvZiBCZWVycyIpICsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgoKIyBOZXVyYWwgTmV0CgoqIENhbiBBQlYsIElCVSwgYW5kIFNSTSBiZSB1c2VkIGluIGEgbmV1cmFsIG5ldCB0byBwcmVkaWN0IGBzdHlsZWAgb3IgYHN0eWxlX2NvbGxhcHNlZGA/CiogSW4gdGhlIGZ1bmN0aW9uLCBzcGVjaWZ5IHRoZSBkYXRhZnJhbWUgYW5kIHRoZSBvdXRjb21lLCBlaXRoZXIgYHN0eWxlYCBvciBgc3R5bGVfY29sbGFwc2VkYDsgdGhlIG9uZSBub3Qgc3BlY2lmaWVkIGFzIGBvdXRjb21lYCB3aWxsIGJlIGRyb3BwZWQKKiBUaGUgcHJlZGljdG9yIGNvbHVtbnMgd2lsbCBiZSBldmVyeXRoaW5nIG5vdCBzcGVjaWZpZWQgaW4gdGhlIHZlY3RvciBgcHJlZGljdG9yX3ZhcnNgCiogVGhlIGZ1bmN0aW9uIHJldHVybnMgdGhlIG91dGNvbWUgdmFyaWFibGUgc2xlZWN0ZWQsIG5ldXJhbCBuZXQgb3V0cHV0LCB2YXJpYWJsZSBpbXBvcnRhbmNlLCB0aGUgcHJlZGljdGlvbiBkYXRhZnJhbWUsIHByZWRpY3Rpb25zLCBhbmQgYWNjdXJhY3kKCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBlY2hvPVRSVUUsIGV2YWw9VFJVRSwgbWVzc2FnZT1GQUxTRX0KCmxpYnJhcnkobm5ldCkKbGlicmFyeShjYXJldCkKCnJ1bl9uZXVyYWxfbmV0IDwtIGZ1bmN0aW9uKGRmLCBvdXRjb21lLCBwcmVkaWN0b3JfdmFycykgewogIG91dCA8LSBsaXN0KG91dGNvbWUgPSBvdXRjb21lKQogIAogICMgQ3JlYXRlIGEgbmV3IGNvbHVtbiBvdXRjb21lOyBpdCdzIHN0eWxlX2NvbGxhcHNlZCBpZiB5b3Ugc2V0IG91dGNvbWUgdG8gc3R5bGVfY29sbGFwc2VkLCBhbmQgc3R5bGUgb3RoZXJ3aXNlCiAgaWYgKG91dGNvbWUgPT0gInN0eWxlX2NvbGxhcHNlZCIpIHsKICAgIGRmW1sib3V0Y29tZSJdXSA8LSBkZltbInN0eWxlX2NvbGxhcHNlZCJdXQogIH0gZWxzZSB7CiAgICBkZltbIm91dGNvbWUiXV0gPC0gZGZbWyJzdHlsZSJdXQogIH0KCiAgZGYkb3V0Y29tZSA8LSBmYWN0b3IoZGYkb3V0Y29tZSkKICAKICBjb2xzX3RvX2tlZXAgPC0gYygib3V0Y29tZSIsIHByZWRpY3Rvcl92YXJzKQogIAogIGRmIDwtIGRmICU+JQogICAgc2VsZWN0XyguZG90cyA9IGNvbHNfdG9fa2VlcCkgJT4lCiAgICBtdXRhdGUocm93ID0gMTpucm93KGRmKSkgJT4lIAogICAgZHJvcGxldmVscygpCgogICMgU2VsZWN0IDgwJSBvZiB0aGUgZGF0YSBmb3IgdHJhaW5pbmcKICBkZl90cmFpbiA8LSBzYW1wbGVfbihkZiwgbnJvdyhkZikqKDAuOCkpCiAgCiAgIyBUaGUgcmVzdCBpcyBmb3IgdGVzdGluZwogIGRmX3Rlc3QgPC0gZGYgJT4lCiAgICBmaWx0ZXIoISAocm93ICVpbiUgZGZfdHJhaW4kcm93KSkgJT4lCiAgICBzZWxlY3QoLXJvdykKICAKICBkZl90cmFpbiA8LSBkZl90cmFpbiAlPiUKICAgIHNlbGVjdCgtcm93KQogIAogICMgQnVpbGQgbXVsdGlub21haWwgbmV1cmFsIG5ldAogIG5uIDwtIG11bHRpbm9tKG91dGNvbWUgfiAuLAogICAgICAgICAgICAgICAgIGRhdGEgPSBkZl90cmFpbiwgbWF4aXQ9NTAwLCB0cmFjZT1GQUxTRSkKCiAgIyBXaGljaCB2YXJpYWJsZXMgYXJlIHRoZSBtb3N0IGltcG9ydGFudCBpbiB0aGUgbmV1cmFsIG5ldD8KICBtb3N0X2ltcG9ydGFudF92YXJzIDwtIHZhckltcChubikKCiAgIyBIb3cgYWNjdXJhdGUgaXMgdGhlIG1vZGVsPyBDb21wYXJlIHByZWRpY3Rpb25zIHRvIG91dGNvbWVzIGZyb20gdGVzdCBkYXRhCiAgbm5fcHJlZHMgPC0gcHJlZGljdChubiwgdHlwZT0iY2xhc3MiLCBuZXdkYXRhID0gZGZfdGVzdCkKICBubl9hY2N1cmFjeSA8LSBwb3N0UmVzYW1wbGUoZGZfdGVzdCRvdXRjb21lLCBubl9wcmVkcykKCiAgb3V0IDwtIGxpc3Qob3V0LCBubiA9IG5uLCBtb3N0X2ltcG9ydGFudF92YXJzID0gbW9zdF9pbXBvcnRhbnRfdmFycywKICAgICAgICAgICAgICBkZl90ZXN0ID0gZGZfdGVzdCwKICAgICAgICAgICAgICBubl9wcmVkcyA9IG5uX3ByZWRzLAogICAgICAgICAgIG5uX2FjY3VyYWN5ID0gbm5fYWNjdXJhY3kpCgogIHJldHVybihvdXQpCn0KCmBgYAoKV2UnbGwgc2V0IHRoZSBkYXRhZnJhbWUgdG8gYmUgYGJlZXJfdG90YWxzYCwgdGhlIHByZWRpY3RvciB2YXJpYWJsZXMgdG8gYmUgdGhlIHZlY3RvciBjb250YWluZWQgaW4gYHBfdmFyc2AsIHRoZSBvdXRjb21lIHRvIGJlIGBzdHlsZV9jb2xsYXBzZWRgCgoKCmBgYHtyLCBlY2hvPVRSVUV9CiMgVGFrZSBvdXQgTkFzCmJ0X29taXQgPC0gYmVlcl90b3RhbHMgJT4lIG5hLm9taXQoKQpgYGAKCmBgYHtyLCBlY2hvPVRSVUUsIGV2YWw9VFJVRSwgZXJyb3I9VFJVRX0KcF92YXJzIDwtIGMoInRvdGFsX2hvcHMiLCAidG90YWxfbWFsdCIsICJhYnYiLCAiaWJ1IiwgInNybSIpCgpubl9jb2xsYXBzZWRfb3V0IDwtIHJ1bl9uZXVyYWxfbmV0KGRmID0gYnRfb21pdCwgb3V0Y29tZSA9ICJzdHlsZV9jb2xsYXBzZWQiLCAKICAgICAgICAgICAgICAgICAgICAgICAgIHByZWRpY3Rvcl92YXJzID0gcF92YXJzKQoKCiMgSG93IGFjY3VyYXRlIHdhcyBpdD8Kbm5fY29sbGFwc2VkX291dCRubl9hY2N1cmFjeQoKIyBXaGF0IHdlcmUgdGhlIG1vc3QgaW1wb3J0YW50IHZhcmlhYmxlcz8Kbm5fY29sbGFwc2VkX291dCRtb3N0X2ltcG9ydGFudF92YXJzCgpgYGAKCgoqIFdoYXQgaWYgd2UgcHJlZGNpdCBgc3R5bGVgIGluc3RlYWQgb2YgYHN0eWxlX2NvbGxhcHNlZGA/CgpgYGB7ciwgZWNobz1UUlVFLCBlcnJvcj1UUlVFfQoKbm5fbm90Y29sbGFwc2VkX291dCA8LSBydW5fbmV1cmFsX25ldChkZiA9IGJ0X29taXQsIG91dGNvbWUgPSAic3R5bGUiLCAKICAgICAgICAgICAgICAgICAgICAgICAgIHByZWRpY3Rvcl92YXJzID0gcF92YXJzKQoKbm5fbm90Y29sbGFwc2VkX291dCRubl9hY2N1cmFjeQoKbm5fbm90Y29sbGFwc2VkX291dCRtb3N0X2ltcG9ydGFudF92YXJzCgpgYGAKCgpBbmQgbm93IGlmIHdlIGFkZCBgZ2xhc3NgIGFzIGEgcHJlZGljdG9yPwpgYGB7ciwgZWNobz1UUlVFLCBlcnJvcj1UUlVFfQoKcF92YXJzX2FkZF9nbGFzcyA8LSBjKCJ0b3RhbF9ob3BzIiwgInRvdGFsX21hbHQiLCAiYWJ2IiwgImlidSIsICJzcm0iLCAiZ2xhc3MiKQoKbm5fY29sbGFwc2VkX291dF9hZGRfZ2xhc3MgPC0gcnVuX25ldXJhbF9uZXQoZGYgPSBiZWVyX2luZ3JlZGllbnRzX2pvaW4sIG91dGNvbWUgPSAic3R5bGVfY29sbGFwc2VkIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkaWN0b3JfdmFycyA9IHBfdmFyc19hZGRfZ2xhc3MpCgpubl9jb2xsYXBzZWRfb3V0X2FkZF9nbGFzcyRubl9hY2N1cmFjeQoKbm5fY29sbGFwc2VkX291dF9hZGRfZ2xhc3MkbW9zdF9pbXBvcnRhbnRfdmFycwoKYGBgCgoKCgojIyMgUmFuZG9tIGZvcmVzdCB3aXRoIGFsbCBpbmdyZWRpZW50cwoKKiBXZSBjYW4gdXNlIGEgcmFuZG9tIGZvcmVzdCB0byBnZXQgZXZlbiBtb3JlIGdyYW51bGFyIHdpdGggaW5ncmVkaWVudHMKICAgICogVGhlIHNwYXJzZSBpbmdyZWRpZW50IGRhdGFmcmFtZSB3YXMgdG9vIGNvbXBsZXggZm9yIHRoZSBtdWx0aW5vbWlhbCBuZXVyYWwgbmV0IGJ1dCB0aGUgYHJhbmdlcmAgY2FuIGhhbmRsZSBzcGFyc2UgZGF0YSBsaWtlIHRoaXMKCiogSGVyZSB3ZSBkb24ndCBpbmNsdWRlIGBnbGFzc2AgYXMgYSBwcmVkaWN0b3IKCmBgYHtyLCBlY2hvPVRSVUV9CgpsaWJyYXJ5KHJhbmdlcikKbGlicmFyeShzdHJpbmdyKQoKYmkgPC0gYmVlcl9pbmdyZWRpZW50c19qb2luICU+JSAKICBzZWxlY3QoLWMoaWQsIG5hbWUsIHN0eWxlLCBob3BzX25hbWUsIG1hbHRfbmFtZSwKICAgICAgICAgICAgIyBkZXNjcmlwdGlvbiwKICAgICAgICAgICAgZ2xhc3MpKSAlPiUgCiAgbXV0YXRlKHJvdyA9IDE6bnJvdyguKSkgJT4lIAogIG5hLm9taXQoKQoKYmkkc3R5bGVfY29sbGFwc2VkIDwtIGZhY3RvcihiaSRzdHlsZV9jb2xsYXBzZWQpCgoKIyByYW5nZXIgY29tcGxhaW5zIGFib3V0IHNwZWNpYWwgY2hhcmFjdGVycyBhbmQgc3BhY2VzIGluIGluZ3JlZGllbnQgY29sdW1uIG5hbWVzLiBUYWtlIHRoZW0gb3V0IGFuZCByZXBsYWNlIHdpdGggZW1wdHkgc3RyaW5nLgpuYW1lcyhiaSkgPC0gdG9sb3dlcihuYW1lcyhiaSkpCm5hbWVzKGJpKSA8LSBzdHJfcmVwbGFjZV9hbGwobmFtZXMoYmkpLCAiICIsICIiKQpuYW1lcyhiaSkgPC0gc3RyX3JlcGxhY2VfYWxsKG5hbWVzKGJpKSwgIihbXFwoXFwpLVxcLycpXSspIiwgIiIpCgojIEtlZXAgODAlIGZvciB0cmFpbmluZwpiaV90cmFpbiA8LSBzYW1wbGVfbihiaSwgbnJvdyhiaSkqKDAuOCkpCgojIFRoZSByZXN0IGlzIGZvciB0ZXN0aW5nCmJpX3Rlc3QgPC0gYmkgJT4lCiAgZmlsdGVyKCEgKHJvdyAlaW4lIGJpX3RyYWluJHJvdykpICU+JQogIGRwbHlyOjpzZWxlY3QoLXJvdykKCmJpX3RyYWluIDwtIGJpX3RyYWluICU+JQogIGRwbHlyOjpzZWxlY3QoLXJvdykgJT4lIAogIHNlbGVjdCgtYCMwNjMwMGApCgpiaV9yZiA8LSByYW5nZXIoc3R5bGVfY29sbGFwc2VkIH4gLiwgZGF0YSA9IGJpX3RyYWluLCBpbXBvcnRhbmNlID0gImltcHVyaXR5Iiwgc2VlZCA9IDExKQpgYGAKCgpPT0IgKG91dCBvZiBiYWcpIHByZWRpY3Rpb24gZXJyb3IgaXMgY2FsY3VsYXRlZCBmcm9tIHRyZWUgc2FtcGxlcyBjb25zdHJ1Y3RlZCBidXQgbm90IHVzZWQgaW4gdHJhaW5pbmcgc2V0OyB0aGVzZSB0cmVlcyBiZWNvbWUgZWZmZWN0aXZlbHkgcGFydCBvZiB0ZXN0IHNldC4KICAgIApgYGB7cn0KYmlfcmYKYGBgCgoKV2UgY2FuIGNvbXBhcmUgcHJlZGljdGVkIGNsYXNzaWZpY2F0aW9uIG9uIHRoZSB0ZXN0IHNldCB0byB0aGVpciBhY3R1YWwgc3R5bGUgY2xhc3NpZmljYXRpb24uCmBgYHtyLCBlY2hvPVRSVUV9CnByZWRfYmlfcmYgPC0gcHJlZGljdChiaV9yZiwgZGF0ID0gYmlfdGVzdCkKIyBrYWJsZSh0YWJsZShiaV90ZXN0JHN0eWxlX2NvbGxhcHNlZCwgcHJlZF9iaV9yZiRwcmVkaWN0aW9ucykpCmBgYAoKClZhcmlhYmxlIGltcG9ydGFuY2UKCiogSW50ZXJlc3RpbmdseSwgQUJWLCBJQlUsIGFuZCBTUk0gYXJlIGFsbCBtdWNoIG1vcmUgaW1wb3J0YW50IGluIHRoZSByYW5kb20gZm9yZXN0IHRoYW4gYHRvdGFsX2hvcHNgIGFuZCBgdG90YWxfbWFsdGAKYGBge3IsIGVjaG89VFJVRX0KCmltcG9ydGFuY2UoYmlfcmYpWzE6MTBdCmBgYAoKCkhvdyBkb2VzIGEgQ1NSRiAoY2FzZS1zcGVjaWZpYyByYW5kb20gZm9yZXN0KSBmYXJlPwoKYGBge3IsIGVjaG89VFJVRX0KCmJpX2NzcmYgPC0gY3NyZihzdHlsZV9jb2xsYXBzZWQgfiAuLCB0cmFpbmluZ19kYXRhID0gYmlfdHJhaW4sIHRlc3RfZGF0YSA9IGJpX3Rlc3QsCiAgICAgICAgICAgICAgICBwYXJhbXMxID0gbGlzdChudW0udHJlZXMgPSA1LCBtdHJ5ID0gNCksCiAgICAgICAgICAgICAgICBwYXJhbXMyID0gbGlzdChudW0udHJlZXMgPSAyKSkKCmNzcmZfYWNjIDwtIHBvc3RSZXNhbXBsZShiaV9jc3JmLCBiaV90ZXN0JHN0eWxlX2NvbGxhcHNlZCkKCmNzcmZfYWNjCmBgYAoKCgoKCiMjIyBGaW5hbCBUaG91Z2h0cwoKCipTdHlsZSBmaXJzdCwgZm9yZ2l2ZW5lc3MgbGF0ZXI/KgoKKiBPbmUgcmVhc29uICBzZWVtcyB0aGF0IGJlZXJzIGFyZSBnZW5lcmFsbHkgYnJld2VkIHdpdGggc3R5bGUgaW4gbWluZCBmaXJzdCAoImxldCdzIG1ha2UgYSBwYWxlIGFsZSIpIHJhdGhlciB0aGFuIGRlY2lkaW5nIHRoZSBiZWVyJ3Mgc3R5bGUgYWZ0ZXIgZGV0ZXJtaW5pbmcgaXRzIGNoYXJhY3RlcmlzdGljcyBhbmQgaWRpb3N5bmNyYXNpZXMgCiAgICAqIEV2ZW4gaWYgdGhlIGJlZXIgdHVybnMgb3V0IG1vcmUgbGlrZSBhIHNvdXIsIGFuZCBpbiBhIGJsaW5kIHRhc3RlIHRlc3QgbWlnaHQgYmUgY2xhc3NpZmllZCBhcyBhIHNvdXIgbW9yZSBvZnRlbiB0aGFuIGEgcGFsZSBhbGUsIGl0IHN0aWxsIGdldHMgdGhlIGxhYmVsIHBhbGUgYWxlCiAgICAqIFRoaXMgbWFrZXMgdGhlIHN0eWxlIGRlZmluaXRpb25zIGJyb2FkZXIgYW5kIGhhcmRlciB0byBwcmVkaWN0CgoKCipGdXR1cmUgRGlyZWN0aW9ucyoKCiogSGllcmFyY2hpY2FsIGNsdXN0ZXJpbmcKKiBJbmNvcnBvcmF0ZSBmbGF2b3IgcHJvZmlsZXMgZm9yIGJlZXJzIHNvdXJjZWQvc2NyYXBlZCBmcm9tIHNvbWV3aGVyZQoqIEltcGxlbWVudCBhIEdBTiB0byBjb21lIHVwIHdpdGggYmVlciBuYW1lcwoqIE1vcmUgb24gdGhlIGhvcHMgZGVlcCBkaXZlOiB3aGljaCBob3BzIGFyZSB1c2VkIG1vc3Qgb2Z0ZW4gaW4gd2hpY2ggc3R5bGVzPwoKCgohW10oLi9wb3VyLmpwZykKCgoqKioKCgpgYGB7ciwgZWNobz1UUlVFfQpzZXNzaW9uSW5mbygpCmBgYAoKCgo=